DropsBrowse Pastes
Login with GitHub

更新提示词

July 3rd, 2025Views: 42(0 unique)C#
using ModelContextProtocol.Server;
using Plugin;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.Extensions.Logging;
using IoTGateway.Model;
using MQTTnet.Protocol;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Packets;
using Newtonsoft.Json;
using System.Threading.Tasks;
using System;
using MQTTnet.Extensions.ManagedClient;
using PluginInterface;
using System.Text;
using System.Threading;

namespace IoTGateway.MCP
{
    /// <summary>
    /// 操作结果响应类
    /// </summary>
    public class OperationResult
    {
        /// <summary>
        /// 操作是否成功
        /// </summary>
        public bool Success { get; set; }

        /// <summary>
        /// 详细消息
        /// </summary>
        public string Message { get; set; }

        /// <summary>
        /// 创建成功的响应
        /// </summary>
        /// <param name="message">成功消息</param>
        /// <returns>操作结果</returns>
        public static OperationResult CreateSuccess(string message)
        {
            return new OperationResult { Success = true, Message = message };
        }

        /// <summary>
        /// 创建失败的响应
        /// </summary>
        /// <param name="message">错误消息</param>
        /// <returns>操作结果</returns>
        public static OperationResult CreateError(string message)
        {
            return new OperationResult { Success = false, Message = message };
        }
    }

    public class VariableInfo
    {
        public object Value { get; set; }
        public bool IsWritable { get; set; }
        public string ProtectType { get; set; }
        public string Expressions { get; set; }
    }

    [McpServerToolType]
    public sealed class DeviceTool
    {
        // 提取重复使用的描述文本为常量
        private const string COMMON_DESCRIPTION = "Always use DevicesList to list devices and decode Unicode names for fuzzy matching; if multiple candidates match, you must ask the user to select—never assume the first is correct; use GetDeviceVariables to check writability and parse expression; After setting the value using SetDeviceVariable, regardless of whether a success message is displayed, you must use GetDeviceVariable to confirm that the value meets the target before the operation can be considered successful. If this verification step is not performed, you cannot call attempt_completion or consider the task complete; absolutely no shell commands like dotnet run—only use MCP tool interface.";

        private readonly ILogger _logger;
        private readonly DeviceService _deviceService;
        private readonly IManagedMqttClient _mqttClient;

        public DeviceTool(
            DeviceService deviceService,
            IManagedMqttClient mqttClient,
            ILogger<DeviceTool> logger)
        {
            _deviceService = deviceService;
            _mqttClient = mqttClient;
            _logger = logger;

            // 记录工具类被创建,用于调试
            _logger.LogInformation("DeviceTool created successfully");
        }

        // 设备列表项结构体
        public class DeviceListItem
        {
            public string Name { get; set; } // 设备实例名
            public bool IsTypeName { get; set; } = false; // 固定为 false,表明是实例名
            public string Type { get; set; } // 设备类型(驱动名)
            public string InstanceId { get; set; }
        }

        /// <summary>
        /// Get the list of sub-devices with meta info.
        /// </summary>
        /// <returns></returns>
        [McpServerTool(Name = "DevicesList"), Description("Get a list of all available device instances." + COMMON_DESCRIPTION)]
        public IEnumerable<DeviceListItem> DevicesList()
        {
            _logger.LogInformation("DevicesList method called");
            var devices = _deviceService.DeviceThreads.Select(x => new DeviceListItem
            {
                Name = x.Device.DeviceName,
                IsTypeName = false, //给大模型看的,防止将设备名称被大模型误判为设备类型导致其他问题
                Type = x.Device.Driver?.DriverName ?? "Unknown",
                InstanceId = x.Device.ID.ToString(),
                //NameHint = "Please verify the device name and variable name using DeviceList-GetDeviceVariables before using the Set function.The user input is in Chinese, so escaping may be necessary.All requirements can be executed using the tools provided by MCP. Do not ask the user or call the command line.Please use the Set function after calling DeviceList and GetDeviceVariables.Finally, please verify that the modified variable values are correct."
            }).ToList();
            _logger.LogInformation($"DevicesList returned {devices.Count} devices: {string.Join(", ", devices.Select(d => d.Name))}");
            return devices;
        }


        /// <summary>
        /// Get the current connection status of the sub-device.
        /// </summary>
        /// <param name="deviceName"></param>
        /// <returns></returns>
        [McpServerTool, Description("Get the current connection status of the sub-device." + COMMON_DESCRIPTION)]
        public bool? GetDeviceStatus(
            [Description("name of device")] string deviceName
            )
        {
            _logger.LogInformation($"GetDeviceStatus method called for device: {deviceName}");
            var status = _deviceService.DeviceThreads.FirstOrDefault(x => x.Device.DeviceName == deviceName)?.Driver.IsConnected;
            _logger.LogInformation($"GetDeviceStatus returned: {status}");
            return status;
        }



        /// <summary>
        /// Get the current value of a variable of a sub-device.
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        [McpServerTool, Description("Get all variables for a specific device." + COMMON_DESCRIPTION)]
        public Dictionary<string, VariableInfo> GetDeviceVariables(
            [Description("name of device")] string deviceName
            )
        {
            _logger.LogInformation($"GetDeviceVariables method called for device: {deviceName}");
            var device = _deviceService.DeviceThreads.FirstOrDefault(x => x.Device.DeviceName == deviceName)?.Device;
            if (device == null)
                return new Dictionary<string, VariableInfo>();

            var variables = device.DeviceVariables.ToDictionary(
                x => x.Name,
                x => new VariableInfo
                {
                    Value = x.CookedValue,
                    IsWritable = x.ProtectType != ProtectTypeEnum.ReadOnly,
                    ProtectType = x.ProtectType.ToString(),
                    Expressions = x.Expressions
                });

            _logger.LogInformation($"GetDeviceVariables returned {variables?.Count ?? 0} variables");
            return variables;
        }



        /// <summary>
        /// Get the current value of a variable of a sub-device.
        /// </summary>
        /// <param name="deviceName"></param>
        /// <param name="variableName"></param>
        /// <returns></returns>
        [McpServerTool, Description("Gets a single variable's details from a device." + COMMON_DESCRIPTION)]
        public async Task<VariableInfo> GetDeviceVariable(
            [Description("name of device")] string deviceName,
            [Description("name of variable")] string variableName)
        {
            _logger.LogInformation($"GetDeviceVariable method called for device: {deviceName}, variable: {variableName}");
            var variable = _deviceService.DeviceThreads.FirstOrDefault(x => x.Device.DeviceName == deviceName)?.Device
                .DeviceVariables.FirstOrDefault(x => x.Name == variableName);

            if (variable == null)
                return null;

            // 创建结果对象
            var result = new VariableInfo
            {
                Value = variable.CookedValue,
                IsWritable = variable.ProtectType != ProtectTypeEnum.ReadOnly,
                ProtectType = variable.ProtectType.ToString(),
                Expressions = variable.Expressions
            };

            try
            {
                // 尝试从MQTT获取最新的遥测数据
                var topic = $"devices/{deviceName}/telemetry";
                _logger.LogInformation($"Attempting to get telemetry data from MQTT topic: {topic}");

                // 创建一个同步事件和临时存储
                using var autoResetEvent = new AutoResetEvent(false);
                object latestValue = null;

                // 临时订阅消息处理
                var messageHandler = new Func<MqttApplicationMessageReceivedEventArgs, Task>(async e =>
                {
                    if (e.ApplicationMessage.Topic == topic)
                    {
                        try
                        {
                            var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
                            _logger.LogInformation($"Received MQTT message: {payload}");

                            // 解析JSON,获取变量值
                            var telemetryData = JsonConvert.DeserializeObject<Dictionary<string, object>>(payload);
                            if (telemetryData != null && telemetryData.TryGetValue(variableName, out var value))
                            {
                                latestValue = value;
                                autoResetEvent.Set(); // 信号收到消息
                            }
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, "Error parsing MQTT message");
                        }
                    }
                    await Task.CompletedTask;
                });

                // 注册临时事件处理器
                _mqttClient.ApplicationMessageReceivedAsync += messageHandler;

                try
                {
                    // 订阅主题 - 修复参数类型问题
                    var topicFilter = new MqttTopicFilter { Topic = topic, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce };
                    await _mqttClient.SubscribeAsync(new[] { topicFilter });

                    // 等待数据,最多3秒
                    if (autoResetEvent.WaitOne(3000))
                    {
                        if (latestValue != null)
                        {
                            result.Value = latestValue;
                            _logger.LogInformation($"Updated variable value from MQTT: {latestValue}");
                        }
                    }
                    else
                    {
                        _logger.LogInformation("No MQTT telemetry received within timeout, using cached value");
                    }
                }
                finally
                {
                    // 取消订阅主题
                    await _mqttClient.UnsubscribeAsync(new[] { topic });
                    // 移除事件处理器
                    _mqttClient.ApplicationMessageReceivedAsync -= messageHandler;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error retrieving telemetry data from MQTT");
                // 出错时使用缓存的值,不影响方法正常返回
            }

            _logger.LogInformation($"GetDeviceVariable returned: {JsonConvert.SerializeObject(result)}");
            return result;
        }



        /// <summary>
        /// Set the value of a variable of a sub-device. This function sends the raw value directly.
        /// </summary>
        /// <param name="deviceName">设备名称</param>
        /// <param name="variableName">变量名称</param>
        /// <param name="value">The raw value to set. The system does not apply any expressions to this value before sending.</param>
        /// <returns>设置是否成功及详细信息</returns>
        [McpServerTool, Description("Set the value of a variable of a sub-device." + COMMON_DESCRIPTION)]
        public async Task<OperationResult> SetDeviceVariable(
            [Description("name of device")] string deviceName,
            [Description("name of variable")] string variableName,
            [Description("The raw value to set")] object value)
        {
            _logger.LogInformation($"SetDeviceVariable method called for device: {deviceName}, variable: {variableName}, value: {value}");
            try
            {
                var deviceThread = _deviceService.DeviceThreads.FirstOrDefault(x => x.Device.DeviceName == deviceName);
                if (deviceThread == null)
                {
                    var errorResult = OperationResult.CreateError($"Device '{deviceName}' not found");
                    _logger.LogWarning($"SetDeviceVariable failed: {errorResult.Message}");
                    return errorResult;
                }

                var deviceVariable = deviceThread.Device.DeviceVariables.FirstOrDefault(x => x.Name == variableName);
                if (deviceVariable == null || deviceVariable.ProtectType == ProtectTypeEnum.ReadOnly)
                {
                    var errorResult = OperationResult.CreateError($"Variable '{variableName}' not found or is read-only");
                    _logger.LogWarning($"SetDeviceVariable failed: {errorResult.Message}");
                    return errorResult;
                }

                // 创建RPC请求
                var rpcRequest = new RpcRequest
                {
                    RequestId = Guid.NewGuid().ToString(),
                    DeviceName = deviceName,
                    Method = "write",
                    Params = new Dictionary<string, object> { { variableName, value } }
                };

                // 触发RPC请求执行
                deviceThread.MyMqttClient_OnExcRpc(this, rpcRequest);

                // 等待一小段时间让RPC请求处理
                await Task.Delay(100);

                var successResult = OperationResult.CreateSuccess($"Successfully set {deviceName}.{variableName} = {value}");
                _logger.LogInformation($"SetDeviceVariable succeeded: {successResult.Message}");
                return successResult;
            }
            catch (Exception ex)
            {
                var errorResult = OperationResult.CreateError($"Failed to set variable: {ex.Message}");
                _logger.LogError(ex, $"SetDeviceVariable failed: {errorResult.Message}");
                return errorResult;
            }
        }

        /// <summary>
        /// Set multiple variable values of a sub-device at once.
        /// </summary>
        /// <param name="deviceName">设备名称</param>
        /// <param name="variables">变量名和值的字典</param>
        /// <returns>设置是否成功及详细信息</returns>
        [McpServerTool, Description("Set multiple variable values of a sub-device at once." + COMMON_DESCRIPTION)]
        public async Task<OperationResult> SetDeviceVariables(
            [Description("name of device")] string deviceName,
            [Description("dictionary of variable names and values")] Dictionary<string, object> variables)
        {
            _logger.LogInformation($"SetDeviceVariables method called for device: {deviceName}, variables: {JsonConvert.SerializeObject(variables)}");
            try
            {
                var deviceThread = _deviceService.DeviceThreads.FirstOrDefault(x => x.Device.DeviceName == deviceName);
                if (deviceThread == null)
                {
                    var errorResult = OperationResult.CreateError($"Device '{deviceName}' not found");
                    _logger.LogWarning($"SetDeviceVariables failed: {errorResult.Message}");
                    return errorResult;
                }

                // 过滤只保留可写的变量
                var writeParams = new Dictionary<string, object>();
                var skippedVariables = new List<string>();
                foreach (var variable in variables)
                {
                    var deviceVariable = deviceThread.Device.DeviceVariables.FirstOrDefault(x => x.Name == variable.Key);
                    if (deviceVariable != null && deviceVariable.ProtectType != ProtectTypeEnum.ReadOnly)
                    {
                        writeParams.Add(variable.Key, variable.Value);
                    }
                    else
                    {
                        skippedVariables.Add(variable.Key);
                    }
                }

                if (writeParams.Count == 0)
                {
                    var errorResult = OperationResult.CreateError($"No writable variables found among: {string.Join(", ", variables.Keys)}");
                    _logger.LogWarning($"SetDeviceVariables failed: {errorResult.Message}");
                    return errorResult;
                }

                // 创建RPC请求
                var rpcRequest = new RpcRequest
                {
                    RequestId = Guid.NewGuid().ToString(),
                    DeviceName = deviceName,
                    Method = "write",
                    Params = writeParams
                };

                // 触发RPC请求执行
                deviceThread.MyMqttClient_OnExcRpc(this, rpcRequest);

                // 等待一小段时间让RPC请求处理
                await Task.Delay(100);

                var successMessage = $"Successfully set {writeParams.Count} variables for device {deviceName}: {string.Join(", ", writeParams.Keys)}";
                if (skippedVariables.Count > 0)
                {
                    successMessage += $". Skipped {skippedVariables.Count} read-only or non-existent variables: {string.Join(", ", skippedVariables)}";
                }

                var successResult = OperationResult.CreateSuccess(successMessage);
                _logger.LogInformation($"SetDeviceVariables succeeded: {successResult.Message}");
                return successResult;
            }
            catch (Exception ex)
            {
                var errorResult = OperationResult.CreateError($"Failed to set variables: {ex.Message}");
                _logger.LogError(ex, $"SetDeviceVariables failed: {errorResult.Message}");
                return errorResult;
            }
        }
    }
}