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 Newtonsoft.Json;
using System.Threading.Tasks;
using System;
using MQTTnet.Extensions.ManagedClient;
using PluginInterface;
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 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; }
public string NameHint { get; set; } = "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."; // 提示大模型直接使用设备名称
}
/// <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.Additional requirements: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.")]
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."
}).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.Additional requirements: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.")]
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.Additional requirements: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.")]
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.Additional requirements: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.")]
public 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
};
_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.Additional requirements: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.")]
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.Additional requirements: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.")]
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;
}
}
}
}