UNPKG

matter-controller-mcp

Version:

An MCP server for Matter Controller, enabling AI agents to control and interact with Matter devices.

906 lines (905 loc) 40.5 kB
#!/usr/bin/env node /** * @license * Copyright 2022-2025 Matter.js Authors * SPDX-License-Identifier: MIT */ /** * Matter Controller MCP Server * Provides Matter device control capabilities through the Model Context Protocol */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { Environment, Logger, singleton, StorageService, Time } from "@matter/main"; import { DescriptorCluster, GeneralCommissioning, OnOff, LevelControl, ColorControl } from "@matter/main/clusters"; import { Ble } from "@matter/main/protocol"; import { ManualPairingCodeCodec, NodeId } from "@matter/main/types"; import { NodeJsBle } from "@matter/nodejs-ble"; import { CommissioningController } from "@project-chip/matter.js"; import { getDeviceTypeDefinitionFromModelByCode } from "@project-chip/matter.js/device"; import { NodeIdUtils, serializeJson, getNodeStructureInfo, findDeviceByEndpoint } from './controllerUtils.js'; // Configure logger const logger = Logger.get("MatterControllerMCP"); Logger.level = process.env.MATTER_LOG_LEVEL || 'info'; // Global variables for the MCP server instance let commissioningController = null; let initializationPromise = null; const environment = Environment.default; const storageService = environment.get(StorageService); const controllerStorage = (await storageService.open("controller")).createContext("data"); const uniqueId = (await controllerStorage.has("uniqueid")) ? await controllerStorage.get("uniqueid") : (environment.vars.string("uniqueid") ?? Time.nowMs().toString()); await controllerStorage.set("uniqueid", uniqueId); const adminFabricLabel = (await controllerStorage.has("fabriclabel")) ? await controllerStorage.get("fabriclabel") : (environment.vars.string("fabriclabel") ?? "MatterControllerMCP"); await controllerStorage.set("fabriclabel", adminFabricLabel); // Initialize BLE if enabled if (environment.vars.get("ble")) { Ble.get = singleton(() => new NodeJsBle({ environment: environment, hciId: environment.vars.number("ble.hci.id"), })); } // Tool input schemas const GetControllerStatusSchema = z.object({}); const CommissionDeviceSchema = z.object({ pairingCode: z.string().optional().describe('Manual pairing code'), longDiscriminator: z.number().optional().describe('Long discriminator value'), setupPin: z.number().optional().describe('Setup PIN'), ip: z.string().optional().describe('Device IP address'), port: z.number().optional().describe('Device port'), ble: z.boolean().default(false).describe('Use BLE for commissioning'), wifiSsid: z.string().optional().describe('WiFi SSID for BLE commissioning'), wifiCredentials: z.string().optional().describe('WiFi credentials for BLE commissioning') }); const GetCommissionedDevicesSchema = z.object({ nodeId: z.string().optional().describe('Optional Node ID to get specific device info') }); const GetDeviceInfoSchema = z.object({ nodeId: z.string().describe('Node ID of the device') }); const ControlOnOffDeviceSchema = z.object({ nodeId: z.string().describe('Node ID of the device'), action: z.enum(['on', 'off', 'toggle']).describe('Action to perform'), endpointId: z.number().default(1).describe('Endpoint ID') }); const ControlLevelDeviceSchema = z.object({ nodeId: z.string().describe('Node ID of the device'), level: z.number().describe('Level value (0-254)'), endpointId: z.number().default(1).describe('Endpoint ID') }); const ControlColorDeviceSchema = z.object({ nodeId: z.string().describe('Node ID of the device'), colorTemperature: z.number().optional().describe('Color temperature in mireds (153-500, lower=cooler, higher=warmer)'), hue: z.number().optional().describe('Hue value (0-254)'), saturation: z.number().optional().describe('Saturation value (0-254)'), endpointId: z.number().default(1).describe('Endpoint ID') }); const DecommissionDeviceSchema = z.object({ nodeId: z.string().describe('Node ID of the device to decommission') }); const ReadAttributesSchema = z.object({ nodeId: z.string().describe('Node ID of the device'), endpointId: z.number().default(1).describe('Endpoint ID'), clusterId: z.number().describe('Cluster ID'), attributeIds: z.array(z.number()).optional().describe('Attribute IDs to read (if not provided, reads all available attributes in the cluster)') }); const WriteAttributesSchema = z.object({ nodeId: z.string().describe('Node ID of the device'), endpointId: z.number().default(1).describe('Endpoint ID'), clusterId: z.number().describe('Cluster ID'), attributes: z.record(z.string(), z.any()).describe('Attributes to write as key-value pairs (key is attribute ID as string, value is the attribute value)') }); const ResetControllerSchema = z.object({ confirm: z.boolean().default(false).describe('Confirmation flag to prevent accidental reset') }); // Tool names enum var ToolName; (function (ToolName) { ToolName["GET_CONTROLLER_STATUS"] = "get_controller_status"; ToolName["COMMISSION_DEVICE"] = "commission_device"; ToolName["DECOMMISSION_DEVICE"] = "decommission_device"; ToolName["GET_COMMISSIONED_DEVICES"] = "get_commissioned_devices"; ToolName["GET_DEVICE_INFO"] = "get_device_info"; ToolName["CONTROL_ONOFF_DEVICE"] = "control_onoff_device"; ToolName["CONTROL_LEVEL_DEVICE"] = "control_level_device"; ToolName["CONTROL_COLOR_DEVICE"] = "control_color_device"; ToolName["WRITE_ATTRIBUTES"] = "write_attributes"; ToolName["READ_ATTRIBUTES"] = "read_attributes"; ToolName["RESET_CONTROLLER"] = "reset_controller"; })(ToolName || (ToolName = {})); // Initialize the Matter controller with lazy loading async function ensureControllerInitialized() { if (commissioningController) { return; // Already initialized } if (initializationPromise) { // Initialization is in progress, wait for it logger.info('Controller initialization already in progress, waiting...'); await initializationPromise; return; } // Start initialization logger.info('Starting Matter controller lazy initialization...'); initializationPromise = initializeController(); try { await initializationPromise; logger.info('Matter controller lazy initialization completed successfully'); } catch (error) { logger.error('Matter controller lazy initialization failed:', error); throw error; } finally { initializationPromise = null; } } async function initializeController() { try { logger.info('Initializing Matter controller...'); commissioningController = new CommissioningController({ environment: { environment: environment, id: uniqueId, }, autoConnect: true, adminFabricLabel: adminFabricLabel, }); await commissioningController.start(); logger.info(`Matter Controller initialized successfully with ID: ${uniqueId}`); } catch (error) { logger.error('Failed to initialize Matter controller:', error); throw error; } } // Helper function to ensure device is connected before operation async function ensureDeviceConnected(nodeIdInput) { // Normalize the nodeId to string format const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(nodeIdInput); if (!commissioningController) { throw new McpError(ErrorCode.InvalidRequest, "Controller not initialized"); } try { const nodeId = NodeIdUtils.parseNodeId(nodeIdString); const node = await commissioningController.getNode(nodeId); // Check if device is already connected if (node.isConnected) { return node; } logger.info(`Device ${nodeIdString} not connected, connecting now...`); // Connect if not already connected if (!node.isConnected) { await node.connect(); } logger.info(`Successfully connected to device ${nodeIdString}`); return node; } catch (error) { logger.error(`Failed to connect to device ${nodeIdString}: ${error}`); throw new McpError(ErrorCode.InternalError, `Failed to connect to device: ${error}`); } } // Tool handler functions async function handleGetControllerStatus(args) { const validatedArgs = GetControllerStatusSchema.parse(args); // Count connected devices using matter.js API let connectedDevicesCount = 0; if (commissioningController) { const nodes = commissioningController.getCommissionedNodes(); for (const nodeId of nodes) { try { const node = await commissioningController.getNode(nodeId); if (node.isConnected) { connectedDevicesCount++; } } catch (error) { // Skip counting if node cannot be accessed } } } return { content: [ { type: 'text', text: JSON.stringify({ uniqueId: uniqueId || 'not set', adminFabricLabel: adminFabricLabel || 'not set', commissioning: commissioningController !== null, commissionedDevices: commissioningController?.getCommissionedNodes().length || 0, connectedDevices: connectedDevicesCount, }, null, 2) } ] }; } async function handleCommissionDevice(args) { const validatedArgs = CommissionDeviceSchema.parse(args); if (!commissioningController) { throw new McpError(ErrorCode.InvalidRequest, "Controller not initialized"); } try { let longDiscriminator; let setupPin; let shortDiscriminator; if (validatedArgs.pairingCode) { const pairingCodeCodec = ManualPairingCodeCodec.decode(validatedArgs.pairingCode); shortDiscriminator = pairingCodeCodec.shortDiscriminator; longDiscriminator = undefined; setupPin = pairingCodeCodec.passcode; } else { longDiscriminator = validatedArgs.longDiscriminator || 3840; setupPin = validatedArgs.setupPin || 20202021; } const commissioningOptions = { regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor, regulatoryCountryCode: "XX", }; if (validatedArgs.ble && validatedArgs.wifiSsid && validatedArgs.wifiCredentials) { commissioningOptions.wifiNetwork = { wifiSsid: validatedArgs.wifiSsid, wifiCredentials: validatedArgs.wifiCredentials, }; } const options = { commissioning: commissioningOptions, discovery: { knownAddress: validatedArgs.ip && validatedArgs.port ? { ip: validatedArgs.ip, port: validatedArgs.port, type: "udp" } : undefined, identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {}, discoveryCapabilities: { ble: validatedArgs.ble || false }, }, passcode: setupPin, }; const nodeId = await commissioningController.commissionNode(options); // Automatically connect to newly commissioned device const nodeIdString = NodeId.toHexString(nodeId); await ensureDeviceConnected(nodeIdString); return { content: [ { type: 'text', text: `Device commissioned successfully with Node ID: ${nodeIdString} and automatically connected` } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to commission device: ${error}`); } } async function handleGetCommissionedDevices(args) { const validatedArgs = GetCommissionedDevicesSchema.parse(args); if (!commissioningController) { throw new McpError(ErrorCode.InvalidRequest, "Controller not initialized"); } try { let nodes = commissioningController.getCommissionedNodes(); let nodeDetails = commissioningController.getCommissionedNodesDetails(); // Return all nodes if no specific nodeId is requested if (validatedArgs.nodeId) { const requestedNodeId = NodeIdUtils.parseNodeId(validatedArgs.nodeId); if (!nodes.includes(requestedNodeId)) { throw new McpError(ErrorCode.InvalidRequest, `Node ${requestedNodeId} is not commissioned`); } nodes = [requestedNodeId]; nodeDetails = nodeDetails.find((node) => node.nodeId === requestedNodeId); } const serializedNodes = nodes.map((nodeId) => NodeId.toHexString(nodeId)); // Add connection status to the details const connectionStatus = []; for (const nodeId of nodes) { try { const node = await commissioningController.getNode(nodeId); connectionStatus.push({ nodeId, connected: node.isConnected, }); } catch (error) { connectionStatus.push({ nodeId, connected: false, error: `Failed to get node status: ${error}` }); } } // Add deviceTypeList to nodeDetails const enhancedNodeDetails = []; for (const node of nodeDetails) { const enhancedNode = { nodeId: node.nodeId, operationalAddress: node.operationalAddress, advertisedName: node.advertisedName, discoveryData: node.discoveryData, deviceMeta: node.deviceData.deviceMeta, deviceTypeList: [], basicInformation: { vendorName: node.deviceData.basicInformation.vendorName, productName: node.deviceData.basicInformation.productName, partNumber: node.deviceData.basicInformation.partNumber, location: node.deviceData.basicInformation.location, productLabel: node.deviceData.basicInformation.productLabel, productUrl: node.deviceData.basicInformation.productUrl, serialNumber: node.deviceData.basicInformation.serialNumber, softwareVersionString: node.deviceData.basicInformation.softwareVersionString, hardwareVersionString: node.deviceData.basicInformation.hardwareVersionString, manufacturingDate: node.deviceData.basicInformation.manufacturingDate, uniqueId: node.deviceData.basicInformation.uniqueId, capabilityMinima: node.deviceData.basicInformation.capabilityMinima, dataModelRevision: node.deviceData.basicInformation.dataModelRevision, specificationVersion: node.deviceData.basicInformation.specificationVersion, maxPathsPerInvoke: node.deviceData.basicInformation.maxPathsPerInvoke, } }; try { const matterNode = await commissioningController.getNode(node.nodeId); const descriptor = matterNode.getRootClusterClient(DescriptorCluster); if (descriptor) { const _deviceTypeList = await descriptor.getDeviceTypeListAttribute(); enhancedNode.deviceTypeList = _deviceTypeList.map((item) => { const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(item.deviceType); return { deviceType: item.deviceType, revision: item.revision, deviceTypeName: deviceTypeDefinition?.name, deviceClass: deviceTypeDefinition?.deviceClass, }; }); } } catch (error) { // If we can't get device type info, keep empty array console.warn(`Failed to get device type info for node ${node.nodeId}:`, error); } enhancedNodeDetails.push(enhancedNode); } const result = { summary: "Matter Commissioned Devices status and basic information", nodeList: serializedNodes, connectionStatus: connectionStatus, details: enhancedNodeDetails }; return { content: [ { type: 'text', text: serializeJson(result) } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to get commissioned devices: ${error}`); } } async function handleGetDeviceInfo(args) { const validatedArgs = GetDeviceInfoSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const nodeStructureInfo = getNodeStructureInfo(node); return { content: [ { type: 'text', text: `NodeId ${nodeIdString} device structure information:\n${nodeStructureInfo}` } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to get device info: ${error}`); } } async function handleControlOnOffDevice(args) { const validatedArgs = ControlOnOffDeviceSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const device = findDeviceByEndpoint(node, validatedArgs.endpointId); if (!device) { throw new McpError(ErrorCode.InvalidRequest, `Endpoint ${validatedArgs.endpointId} not found`); } const onOff = device.getClusterClient(OnOff.Complete); if (!onOff) { throw new McpError(ErrorCode.InvalidRequest, `OnOff cluster not available on device ${nodeIdString}`); } let result; switch (validatedArgs.action) { case 'on': await onOff.on(); result = 'Device turned on'; break; case 'off': await onOff.off(); result = 'Device turned off'; break; case 'toggle': await onOff.toggle(); result = 'Device toggled'; break; default: throw new McpError(ErrorCode.InvalidRequest, `Invalid action: ${validatedArgs.action}`); } return { content: [ { type: 'text', text: result } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to control device: ${error}`); } } async function handleControlLevelDevice(args) { const validatedArgs = ControlLevelDeviceSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const device = findDeviceByEndpoint(node, validatedArgs.endpointId); if (!device) { throw new McpError(ErrorCode.InvalidRequest, `Endpoint ${validatedArgs.endpointId} not found`); } const levelControl = device.getClusterClient(LevelControl.Complete); if (!levelControl) { throw new McpError(ErrorCode.InvalidRequest, `LevelControl cluster not available on device ${nodeIdString}`); } await levelControl.moveToLevel({ level: validatedArgs.level, transitionTime: 0, optionsMask: {}, optionsOverride: {} }); return { content: [ { type: 'text', text: `Device level set to ${validatedArgs.level}` } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to control device level: ${error}`); } } async function handleControlColorDevice(args) { const validatedArgs = ControlColorDeviceSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const device = findDeviceByEndpoint(node, validatedArgs.endpointId); if (!device) { throw new McpError(ErrorCode.InvalidRequest, `Endpoint ${validatedArgs.endpointId} not found`); } const colorControl = device.getClusterClient(ColorControl.Complete); if (!colorControl) { throw new McpError(ErrorCode.InvalidRequest, `ColorControl cluster not available on device ${nodeIdString}`); } let result = ''; // Handle color temperature control (for warm/cool white) if (validatedArgs.colorTemperature !== undefined) { await colorControl.moveToColorTemperature({ colorTemperatureMireds: validatedArgs.colorTemperature, transitionTime: 0, optionsMask: {}, optionsOverride: {} }); result += `Color temperature set to ${validatedArgs.colorTemperature} mireds`; } // Handle hue and saturation control (for colored lights) if (validatedArgs.hue !== undefined && validatedArgs.saturation !== undefined) { await colorControl.moveToHueAndSaturation({ hue: validatedArgs.hue, saturation: validatedArgs.saturation, transitionTime: 0, optionsMask: {}, optionsOverride: {} }); result += `${result ? ' and ' : ''}Hue set to ${validatedArgs.hue}, Saturation set to ${validatedArgs.saturation}`; } else if (validatedArgs.hue !== undefined) { await colorControl.moveToHue({ hue: validatedArgs.hue, direction: 0, // 0 = shortest distance transitionTime: 0, optionsMask: {}, optionsOverride: {} }); result += `${result ? ' and ' : ''}Hue set to ${validatedArgs.hue}`; } else if (validatedArgs.saturation !== undefined) { await colorControl.moveToSaturation({ saturation: validatedArgs.saturation, transitionTime: 0, optionsMask: {}, optionsOverride: {} }); result += `${result ? ' and ' : ''}Saturation set to ${validatedArgs.saturation}`; } if (!result) { throw new McpError(ErrorCode.InvalidRequest, 'At least one color parameter (colorTemperature, hue, or saturation) must be provided'); } return { content: [ { type: 'text', text: result } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to control device color: ${error}`); } } async function handleDecommissionDevice(args) { const validatedArgs = DecommissionDeviceSchema.parse(args); if (!commissioningController) { throw new McpError(ErrorCode.InvalidRequest, "Controller not initialized"); } try { const nodeId = NodeIdUtils.parseNodeId(validatedArgs.nodeId); const nodeIdString = NodeId.toHexString(nodeId); // Check if the node is commissioned const commissionedNodes = commissioningController.getCommissionedNodes(); const nodeExists = commissionedNodes.some(commissionedNodeId => NodeId.toHexString(commissionedNodeId) === nodeIdString); if (!nodeExists) { throw new McpError(ErrorCode.InvalidRequest, `Node ${nodeIdString} is not commissioned`); } // Ensure the device is connected before attempting to remove it let node = null; try { logger.info(`Connecting to device ${nodeIdString} for decommissioning...`); node = await ensureDeviceConnected(nodeIdString); } catch (error) { logger.warn(`Failed to connect to device ${nodeIdString} for decommissioning: ${error}`); // Continue with removal even if connection fails } // Remove the node from the commissioned nodes while connected try { logger.info(`Removing node ${nodeIdString} from commissioning controller...`); await commissioningController.removeNode(nodeId); logger.info(`Successfully removed node ${nodeIdString} from commissioning controller`); } catch (error) { logger.warn(`Failed to remove node ${nodeIdString} from commissioning controller: ${error}`); // Continue with cleanup even if removal fails } // Now disconnect and clean up local state if (node && node.isConnected) { try { logger.info(`Disconnecting from node ${nodeIdString}...`); await node.disconnect(); logger.info(`Successfully disconnected from node ${nodeIdString}`); } catch (error) { logger.warn(`Failed to disconnect from node ${nodeIdString}: ${error}`); } } return { content: [ { type: 'text', text: `Device with Node ID ${validatedArgs.nodeId} has been decommissioned successfully. Note: For complete removal, the device should also be factory reset.` } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to decommission device: ${error}`); } } async function handleWriteAttributes(args) { const validatedArgs = WriteAttributesSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const devices = node.getDevices(); const device = devices.find((d) => d.number === validatedArgs.endpointId); if (!device) { throw new McpError(ErrorCode.InvalidRequest, `Endpoint ${validatedArgs.endpointId} not found`); } // Get the cluster client for the specified cluster ID const clusterClient = device.getClusterClient({ id: validatedArgs.clusterId }); if (!clusterClient) { throw new McpError(ErrorCode.InvalidRequest, `Cluster ${validatedArgs.clusterId} not available on device ${nodeIdString} endpoint ${validatedArgs.endpointId}`); } const results = { nodeId: nodeIdString, endpointId: validatedArgs.endpointId, clusterId: validatedArgs.clusterId, writeResults: {} }; // Write each attribute for (const [attributeIdStr, value] of Object.entries(validatedArgs.attributes)) { const attributeId = parseInt(attributeIdStr); try { await clusterClient.setAttribute(attributeId, value); results.writeResults[attributeId] = { success: true, message: 'Attribute written successfully' }; logger.info(`Successfully wrote attribute ${attributeId} with value ${JSON.stringify(value)} to cluster ${validatedArgs.clusterId}`); } catch (error) { logger.error(`Failed to write attribute ${attributeId} to cluster ${validatedArgs.clusterId}: ${error}`); results.writeResults[attributeId] = { success: false, error: `Failed to write: ${error}` }; } } // Convert any BigInt values to strings for JSON serialization const processedResults = serializeJson(results); return { content: [ { type: 'text', text: `Attributes write results for device ${nodeIdString}:\n${processedResults}` } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to write attributes: ${error}`); } } async function handleReadAttributes(args) { const validatedArgs = ReadAttributesSchema.parse(args); // Use the unified validation method const nodeIdString = NodeIdUtils.validateAndNormalizeNodeId(validatedArgs.nodeId); const node = await ensureDeviceConnected(nodeIdString); try { const allAttributes = await node.readAllAttributes(); // Filter attributes by endpoint and cluster const clusterAttributes = allAttributes.filter(item => item.path.endpointId === validatedArgs.endpointId && item.path.clusterId === validatedArgs.clusterId); let filteredAttributes; if (validatedArgs.attributeIds && validatedArgs.attributeIds.length > 0) { // Read only specified attributes filteredAttributes = clusterAttributes.filter(item => validatedArgs.attributeIds.includes(item.path.attributeId)); // Check if all requested attributes were found const foundAttributeIds = filteredAttributes.map(attr => attr.path.attributeId); const missingAttributes = validatedArgs.attributeIds.filter(id => !foundAttributeIds.includes(id)); if (missingAttributes.length > 0) { logger.warn(`Some requested attributes were not found: ${missingAttributes.join(', ')}`); } } else { // Read all attributes in the cluster filteredAttributes = clusterAttributes; } if (filteredAttributes.length === 0) { const message = validatedArgs.attributeIds ? `No requested attributes found on endpoint ${validatedArgs.endpointId} of cluster ${validatedArgs.clusterId}` : `No attributes found on endpoint ${validatedArgs.endpointId} of cluster ${validatedArgs.clusterId}`; throw new McpError(ErrorCode.InvalidRequest, message); } // Transform raw Matter.js attribute data into optimized structure let attributes = []; filteredAttributes.map(attr => (attributes.push({ id: attr.path.attributeId, value: attr.value, version: attr.version }))); const result = { summary: `Attributes read results for device ${nodeIdString} (endpoint ${validatedArgs.endpointId}, cluster ${validatedArgs.clusterId})`, nodeId: nodeIdString, endpointId: validatedArgs.endpointId, clusterId: validatedArgs.clusterId, attributes, }; return { content: [ { type: 'text', text: serializeJson(result) } ] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to read attributes: ${error}`); } } async function handleResetController(args) { const validatedArgs = ResetControllerSchema.parse(args); if (!validatedArgs.confirm) { throw new McpError(ErrorCode.InvalidRequest, "Reset operation requires confirmation. Set 'confirm' to true to proceed."); } try { logger.info('Starting controller reset process...'); // Close existing controller and connections if they exist if (commissioningController) { try { logger.info('Closing existing controller connections...'); const nodes = commissioningController.getCommissionedNodes(); for (const nodeId of nodes) { try { const node = await commissioningController.getNode(nodeId); if (node.isConnected) { await commissioningController.removeNode(nodeId); } } catch (error) { logger.warn(`Error disconnecting from node ${nodeId}:`, error); } } await commissioningController.resetStorage(); await commissioningController.close(); logger.info('Controller closed successfully'); } catch (error) { logger.warn('Error closing existing controller:', error); } } // Reset global variables commissioningController = null; initializationPromise = null; logger.info('Controller reset completed successfully'); return { content: [ { type: 'text', text: 'Matter controller has been reset successfully. All storage data and connections have been cleared. The controller will be re-initialized on the next operation.' } ] }; } catch (error) { logger.error('Failed to reset controller:', error); throw new McpError(ErrorCode.InternalError, `Failed to reset controller: ${error}`); } } // Main createServer function export const createServer = () => { const server = new Server({ name: 'matter-controller', version: '0.1.0', description: 'Matter device controller MCP server', }, { capabilities: { tools: {}, }, }); // Setup tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: ToolName.GET_CONTROLLER_STATUS, description: 'Get the current status of the Matter controller (automatically initialized on startup)', inputSchema: zodToJsonSchema(GetControllerStatusSchema) }, { name: ToolName.COMMISSION_DEVICE, description: 'Commission a new Matter device', inputSchema: zodToJsonSchema(CommissionDeviceSchema) }, { name: ToolName.GET_COMMISSIONED_DEVICES, description: 'Get list of commissioned devices (optionally filter by specific nodeId)', inputSchema: zodToJsonSchema(GetCommissionedDevicesSchema) }, { name: ToolName.GET_DEVICE_INFO, description: 'Get detailed information about the device and its capabilities structure', inputSchema: zodToJsonSchema(GetDeviceInfoSchema) }, { name: ToolName.CONTROL_ONOFF_DEVICE, description: 'Control on/off state of a device', inputSchema: zodToJsonSchema(ControlOnOffDeviceSchema) }, { name: ToolName.CONTROL_LEVEL_DEVICE, description: 'Control level (brightness/dimming) of a device', inputSchema: zodToJsonSchema(ControlLevelDeviceSchema) }, { name: ToolName.CONTROL_COLOR_DEVICE, description: 'Control color temperature and color of a device', inputSchema: zodToJsonSchema(ControlColorDeviceSchema) }, { name: ToolName.DECOMMISSION_DEVICE, description: 'Decommission a commissioned Matter device', inputSchema: zodToJsonSchema(DecommissionDeviceSchema) }, { name: ToolName.WRITE_ATTRIBUTES, description: 'Write attributes to a device cluster (supports batch writing)', inputSchema: zodToJsonSchema(WriteAttributesSchema) }, { name: ToolName.READ_ATTRIBUTES, description: 'Read attributes from a device cluster (can read specific attributes or all attributes in a cluster)', inputSchema: zodToJsonSchema(ReadAttributesSchema) }, { name: ToolName.RESET_CONTROLLER, description: 'Reset the Matter controller and clear all storage data (requires confirmation)', inputSchema: zodToJsonSchema(ResetControllerSchema) } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { await ensureControllerInitialized(); switch (name) { case ToolName.GET_CONTROLLER_STATUS: return await handleGetControllerStatus(args); case ToolName.COMMISSION_DEVICE: return await handleCommissionDevice(args); case ToolName.GET_COMMISSIONED_DEVICES: return await handleGetCommissionedDevices(args); case ToolName.GET_DEVICE_INFO: return await handleGetDeviceInfo(args); case ToolName.CONTROL_ONOFF_DEVICE: return await handleControlOnOffDevice(args); case ToolName.CONTROL_LEVEL_DEVICE: return await handleControlLevelDevice(args); case ToolName.CONTROL_COLOR_DEVICE: return await handleControlColorDevice(args); case ToolName.DECOMMISSION_DEVICE: return await handleDecommissionDevice(args); case ToolName.WRITE_ATTRIBUTES: return await handleWriteAttributes(args); case ToolName.READ_ATTRIBUTES: return await handleReadAttributes(args); case ToolName.RESET_CONTROLLER: return await handleResetController(args); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { logger.error(`Error handling tool ${name}:`, error); throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`); } }); server.onerror = (error) => { logger.error('MCP Server error:', error); }; // Cleanup function const cleanup = async () => { logger.info('Cleaning up Matter Controller MCP server...'); // Close all connections if (commissioningController) { try { // First disconnect all nodes const nodes = commissioningController.getCommissionedNodes(); for (const nodeId of nodes) { try { const node = await commissioningController.getNode(nodeId); if (node.isConnected) { await node.disconnect(); } } catch (error) { logger.error(`Error disconnecting from node ${nodeId}:`, error); } } // Then close the controller await commissioningController.close(); } catch (error) { logger.error('Error closing commissioning controller:', error); } } }; console.log('Matter controller created'); return { server, cleanup }; };