From 92aab068a3cd48a52aaf43e598b6302c2a91956e Mon Sep 17 00:00:00 2001 From: Zhanghu Date: Thu, 13 Nov 2025 16:07:34 +0800 Subject: [PATCH] =?UTF-8?q?AspNetCoreMcpServer=20=E8=83=BD=E5=8A=A0?= =?UTF-8?q?=E5=88=B0=20dify=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- .idea/.idea.McpServerCSharp/.idea/.gitignore | 13 + .../.idea.McpServerCSharp/.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 + .idea/.idea.McpServerCSharp/.idea/vcs.xml | 6 + .../AspNetCoreMcpServer.csproj | 14 + AspNetCoreMcpServer/AspNetCoreMcpServer.http | 6 + AspNetCoreMcpServer/Program.cs | 20 + .../Properties/launchSettings.json | 23 + .../Resources/SimpleResourceType.cs | 11 + AspNetCoreMcpServer/Tools/EchoTool.cs | 14 + AspNetCoreMcpServer/Tools/IotDeviceTools.cs | 526 ++++++++++++++++++ AspNetCoreMcpServer/Tools/SampleLlmTool.cs | 31 ++ AspNetCoreMcpServer/Tools/WeatherTools.cs | 366 ++++++++++++ .../appsettings.Development.json | 8 + AspNetCoreMcpServer/appsettings.json | 9 + .../ConsoleEchoMcpServer.csproj | 3 +- ConsoleEchoMcpServer/Program.cs | 15 +- ConsoleEchoMcpServer/Tools/EchoTool.cs | 14 + McpServerCSharp.sln | 6 + 20 files changed, 1096 insertions(+), 4 deletions(-) create mode 100644 .idea/.idea.McpServerCSharp/.idea/.gitignore create mode 100644 .idea/.idea.McpServerCSharp/.idea/encodings.xml create mode 100644 .idea/.idea.McpServerCSharp/.idea/indexLayout.xml create mode 100644 .idea/.idea.McpServerCSharp/.idea/vcs.xml create mode 100644 AspNetCoreMcpServer/AspNetCoreMcpServer.csproj create mode 100644 AspNetCoreMcpServer/AspNetCoreMcpServer.http create mode 100644 AspNetCoreMcpServer/Program.cs create mode 100644 AspNetCoreMcpServer/Properties/launchSettings.json create mode 100644 AspNetCoreMcpServer/Resources/SimpleResourceType.cs create mode 100644 AspNetCoreMcpServer/Tools/EchoTool.cs create mode 100644 AspNetCoreMcpServer/Tools/IotDeviceTools.cs create mode 100644 AspNetCoreMcpServer/Tools/SampleLlmTool.cs create mode 100644 AspNetCoreMcpServer/Tools/WeatherTools.cs create mode 100644 AspNetCoreMcpServer/appsettings.Development.json create mode 100644 AspNetCoreMcpServer/appsettings.json create mode 100644 ConsoleEchoMcpServer/Tools/EchoTool.cs diff --git a/.gitignore b/.gitignore index add57be..8454050 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +/*.user diff --git a/.idea/.idea.McpServerCSharp/.idea/.gitignore b/.idea/.idea.McpServerCSharp/.idea/.gitignore new file mode 100644 index 0000000..ce6a1da --- /dev/null +++ b/.idea/.idea.McpServerCSharp/.idea/.gitignore @@ -0,0 +1,13 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# Rider 忽略的文件 +/projectSettingsUpdater.xml +/modules.xml +/.idea.McpServerCSharp.iml +/contentModel.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.McpServerCSharp/.idea/encodings.xml b/.idea/.idea.McpServerCSharp/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.McpServerCSharp/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.McpServerCSharp/.idea/indexLayout.xml b/.idea/.idea.McpServerCSharp/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.McpServerCSharp/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.McpServerCSharp/.idea/vcs.xml b/.idea/.idea.McpServerCSharp/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.McpServerCSharp/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj b/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj new file mode 100644 index 0000000..1914d45 --- /dev/null +++ b/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/AspNetCoreMcpServer/AspNetCoreMcpServer.http b/AspNetCoreMcpServer/AspNetCoreMcpServer.http new file mode 100644 index 0000000..662e71c --- /dev/null +++ b/AspNetCoreMcpServer/AspNetCoreMcpServer.http @@ -0,0 +1,6 @@ +@AspNetCoreMcpServer_HostAddress = http://localhost:5283 + +GET {{AspNetCoreMcpServer_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/AspNetCoreMcpServer/Program.cs b/AspNetCoreMcpServer/Program.cs new file mode 100644 index 0000000..50cd99b --- /dev/null +++ b/AspNetCoreMcpServer/Program.cs @@ -0,0 +1,20 @@ +using AspNetCoreMcpServer.Resources; +using AspNetCoreMcpServer.Tools; + +var builder = WebApplication.CreateBuilder(args); + +// 添加 HttpClientFactory 支持 +builder.Services.AddHttpClient(); + +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithResources(); + + +var app = builder.Build(); +app.MapMcp(); +app.Run(); \ No newline at end of file diff --git a/AspNetCoreMcpServer/Properties/launchSettings.json b/AspNetCoreMcpServer/Properties/launchSettings.json new file mode 100644 index 0000000..97436a0 --- /dev/null +++ b/AspNetCoreMcpServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://0.0.0.0:5284", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://0.0.0.0:7154;http://0.0.0.0:5284", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCoreMcpServer/Resources/SimpleResourceType.cs b/AspNetCoreMcpServer/Resources/SimpleResourceType.cs new file mode 100644 index 0000000..60dfe62 --- /dev/null +++ b/AspNetCoreMcpServer/Resources/SimpleResourceType.cs @@ -0,0 +1,11 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpServer.Resources; + +[McpServerResourceType] +public class SimpleResourceType +{ + [McpServerResource, Description("A direct text resource")] + public static string DirectTextResource() => "This is a direct resource"; +} \ No newline at end of file diff --git a/AspNetCoreMcpServer/Tools/EchoTool.cs b/AspNetCoreMcpServer/Tools/EchoTool.cs new file mode 100644 index 0000000..6e6c179 --- /dev/null +++ b/AspNetCoreMcpServer/Tools/EchoTool.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +namespace AspNetCoreMcpServer.Tools; + +[McpServerToolType] +public sealed class EchoTool +{ + [McpServerTool, Description("Echoes the message back to the client.")] + public static string Echo(string message) => $"Hello from C#: {message}"; + + [McpServerTool, Description("Echoes in reverse the message sent.")] + public static string ReverseEcho(string message) => string.Concat(message.Reverse()); +} \ No newline at end of file diff --git a/AspNetCoreMcpServer/Tools/IotDeviceTools.cs b/AspNetCoreMcpServer/Tools/IotDeviceTools.cs new file mode 100644 index 0000000..180944e --- /dev/null +++ b/AspNetCoreMcpServer/Tools/IotDeviceTools.cs @@ -0,0 +1,526 @@ +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; + +namespace AspNetCoreMcpServer.Tools; + +[McpServerToolType] +public sealed class IotDeviceTools +{ +// 模拟设备状态存储 + private static readonly Dictionary _deviceStates = new(); + + public IotDeviceTools() + { + // 初始化一些默认设备 + InitializeDefaultDevices(); + } + + private void InitializeDefaultDevices() + { + _deviceStates["living_room_light"] = new DeviceState + { + Id = "living_room_light", + Name = "客厅灯", + Type = "light", + IsOn = false, + Brightness = 100, + Temperature = 4000 + }; + + _deviceStates["bedroom_curtain"] = new DeviceState + { + Id = "bedroom_curtain", + Name = "卧室窗帘", + Type = "curtain", + IsOn = false, + Position = 0 + }; + + _deviceStates["living_room_ac"] = new DeviceState + { + Id = "living_room_ac", + Name = "客厅空调", + Type = "air_conditioner", + IsOn = false, + Temperature = 26, + Mode = "cool", + FanSpeed = "medium" + }; + } + + // ==================== 灯光控制 ==================== + + [McpServerTool, Description("控制灯光的开关状态")] + [McpMeta("category", "iot")] + [McpMeta("device", "light")] + public async Task ControlLight( + [Description("设备ID,例如: living_room_light, bedroom_light")] string deviceId, + [Description("开关状态: on 或 off")] string action) + { + await Task.Delay(100); // 模拟网络延迟 + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "light") + { + return $"错误: {deviceId} 不是灯光设备"; + } + + bool turnOn = action.ToLower() == "on"; + device.IsOn = turnOn; + + return $""" + 设备控制成功 + 设备: {device.Name} ({deviceId}) + 状态: {(turnOn ? "已开启" : "已关闭")} + 亮度: {device.Brightness}% + 色温: {device.Temperature}K + """; + } + + [McpServerTool, Description("调节灯光亮度")] + [McpMeta("category", "iot")] + [McpMeta("device", "light")] + public async Task SetLightBrightness( + [Description("设备ID")] string deviceId, + [Description("亮度值 (0-100)")] int brightness) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "light") + { + return $"错误: {deviceId} 不是灯光设备"; + } + + if (brightness < 0 || brightness > 100) + { + return "错误: 亮度值必须在 0-100 之间"; + } + + device.Brightness = brightness; + if (brightness > 0 && !device.IsOn) + { + device.IsOn = true; + } + + return $""" + 亮度调节成功 + 设备: {device.Name} + 当前亮度: {brightness}% + 状态: {(device.IsOn ? "开启" : "关闭")} + """; + } + + [McpServerTool, Description("调节灯光色温")] + [McpMeta("category", "iot")] + [McpMeta("device", "light")] + public async Task SetLightTemperature( + [Description("设备ID")] string deviceId, + [Description("色温值 (2700-6500K)")] int temperature) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "light") + { + return $"错误: {deviceId} 不是灯光设备"; + } + + if (temperature < 2700 || temperature > 6500) + { + return "错误: 色温值必须在 2700-6500K 之间"; + } + + device.Temperature = temperature; + + string tempDesc = temperature < 3500 ? "暖光" : temperature < 5000 ? "自然光" : "冷光"; + + return $""" + 色温调节成功 + 设备: {device.Name} + 当前色温: {temperature}K ({tempDesc}) + 亮度: {device.Brightness}% + """; + } + + // ==================== 窗帘控制 ==================== + + [McpServerTool, Description("控制窗帘的开关")] + [McpMeta("category", "iot")] + [McpMeta("device", "curtain")] + public async Task ControlCurtain( + [Description("设备ID,例如: bedroom_curtain")] string deviceId, + [Description("操作: open(打开), close(关闭), stop(停止)")] string action) + { + await Task.Delay(200); // 模拟窗帘运行时间 + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "curtain") + { + return $"错误: {deviceId} 不是窗帘设备"; + } + + string result = action.ToLower() switch + { + "open" => HandleCurtainOpen(device), + "close" => HandleCurtainClose(device), + "stop" => HandleCurtainStop(device), + _ => "错误: 无效的操作,请使用 open, close 或 stop" + }; + + return result; + } + + [McpServerTool, Description("设置窗帘打开的位置")] + [McpMeta("category", "iot")] + [McpMeta("device", "curtain")] + public async Task SetCurtainPosition( + [Description("设备ID")] string deviceId, + [Description("位置百分比 (0-100),0表示完全关闭,100表示完全打开")] int position) + { + await Task.Delay(150); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "curtain") + { + return $"错误: {deviceId} 不是窗帘设备"; + } + + if (position < 0 || position > 100) + { + return "错误: 位置值必须在 0-100 之间"; + } + + device.Position = position; + device.IsOn = position > 0; + + return $""" + 窗帘位置调节成功 + 设备: {device.Name} + 当前位置: {position}% + 状态: {(position == 0 ? "完全关闭" : position == 100 ? "完全打开" : "部分打开")} + """; + } + + // ==================== 空调控制 ==================== + + [McpServerTool, Description("控制空调的开关")] + [McpMeta("category", "iot")] + [McpMeta("device", "air_conditioner")] + public async Task ControlAirConditioner( + [Description("设备ID,例如: living_room_ac")] string deviceId, + [Description("开关状态: on 或 off")] string action) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "air_conditioner") + { + return $"错误: {deviceId} 不是空调设备"; + } + + bool turnOn = action.ToLower() == "on"; + device.IsOn = turnOn; + + return $""" + 空调控制成功 + 设备: {device.Name} + 状态: {(turnOn ? "已开启" : "已关闭")} + {(turnOn ? $"温度: {device.Temperature}°C\n模式: {GetModeDescription(device.Mode)}\n风速: {GetFanSpeedDescription(device.FanSpeed)}" : "")} + """; + } + + [McpServerTool, Description("设置空调温度")] + [McpMeta("category", "iot")] + [McpMeta("device", "air_conditioner")] + public async Task SetAirConditionerTemperature( + [Description("设备ID")] string deviceId, + [Description("目标温度 (16-30°C)")] int temperature) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "air_conditioner") + { + return $"错误: {deviceId} 不是空调设备"; + } + + if (temperature < 16 || temperature > 30) + { + return "错误: 温度必须在 16-30°C 之间"; + } + + device.Temperature = temperature; + if (!device.IsOn) + { + device.IsOn = true; + } + + return $""" + 空调温度设置成功 + 设备: {device.Name} + 目标温度: {temperature}°C + 当前模式: {GetModeDescription(device.Mode)} + 风速: {GetFanSpeedDescription(device.FanSpeed)} + """; + } + + [McpServerTool, Description("设置空调模式")] + [McpMeta("category", "iot")] + [McpMeta("device", "air_conditioner")] + public async Task SetAirConditionerMode( + [Description("设备ID")] string deviceId, + [Description("模式: cool(制冷), heat(制热), fan(送风), dry(除湿), auto(自动)")] string mode) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "air_conditioner") + { + return $"错误: {deviceId} 不是空调设备"; + } + + var validModes = new[] { "cool", "heat", "fan", "dry", "auto" }; + if (!validModes.Contains(mode.ToLower())) + { + return $"错误: 无效的模式。有效模式: {string.Join(", ", validModes)}"; + } + + device.Mode = mode.ToLower(); + + return $""" + 空调模式设置成功 + 设备: {device.Name} + 当前模式: {GetModeDescription(device.Mode)} + 温度: {device.Temperature}°C + 风速: {GetFanSpeedDescription(device.FanSpeed)} + """; + } + + [McpServerTool, Description("设置空调风速")] + [McpMeta("category", "iot")] + [McpMeta("device", "air_conditioner")] + public async Task SetAirConditionerFanSpeed( + [Description("设备ID")] string deviceId, + [Description("风速: low(低), medium(中), high(高), auto(自动)")] string fanSpeed) + { + await Task.Delay(100); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}"; + } + + var device = _deviceStates[deviceId]; + if (device.Type != "air_conditioner") + { + return $"错误: {deviceId} 不是空调设备"; + } + + var validSpeeds = new[] { "low", "medium", "high", "auto" }; + if (!validSpeeds.Contains(fanSpeed.ToLower())) + { + return $"错误: 无效的风速。有效风速: {string.Join(", ", validSpeeds)}"; + } + + device.FanSpeed = fanSpeed.ToLower(); + + return $""" + 空调风速设置成功 + 设备: {device.Name} + 当前风速: {GetFanSpeedDescription(device.FanSpeed)} + 模式: {GetModeDescription(device.Mode)} + 温度: {device.Temperature}°C + """; + } + + // ==================== 通用查询 ==================== + + [McpServerTool, Description("获取所有IoT设备的状态")] + [McpMeta("category", "iot")] + [McpMeta("device", "all")] + public async Task GetAllDevicesStatus() + { + await Task.Delay(50); + + var statusList = _deviceStates.Values.Select(device => + { + string status = device.Type switch + { + "light" => $"开关: {(device.IsOn ? "开" : "关")}, 亮度: {device.Brightness}%, 色温: {device.Temperature}K", + "curtain" => $"位置: {device.Position}%, 状态: {(device.Position == 0 ? "关闭" : device.Position == 100 ? "打开" : "部分打开")}", + "air_conditioner" => $"开关: {(device.IsOn ? "开" : "关")}, 温度: {device.Temperature}°C, 模式: {GetModeDescription(device.Mode)}, 风速: {GetFanSpeedDescription(device.FanSpeed)}", + _ => "未知设备类型" + }; + + return $""" + [{device.Type.ToUpper()}] {device.Name} ({device.Id}) + {status} + """; + }); + + return $"当前设备总数: {_deviceStates.Count}\n\n" + string.Join("\n---\n", statusList); + } + + [McpServerTool, Description("获取指定设备的详细状态")] + [McpMeta("category", "iot")] + [McpMeta("device", "all")] + public async Task GetDeviceStatus( + [Description("设备ID")] string deviceId) + { + await Task.Delay(50); + + if (!_deviceStates.ContainsKey(deviceId)) + { + return $"错误: 找不到设备 {deviceId}\n\n可用设备: {string.Join(", ", _deviceStates.Keys)}"; + } + + var device = _deviceStates[deviceId]; + + string details = device.Type switch + { + "light" => $""" + 开关状态: {(device.IsOn ? "开启" : "关闭")} + 亮度: {device.Brightness}% + 色温: {device.Temperature}K ({(device.Temperature < 3500 ? "暖光" : device.Temperature < 5000 ? "自然光" : "冷光")}) + """, + "curtain" => $""" + 位置: {device.Position}% + 状态: {(device.Position == 0 ? "完全关闭" : device.Position == 100 ? "完全打开" : "部分打开")} + """, + "air_conditioner" => $""" + 开关状态: {(device.IsOn ? "开启" : "关闭")} + 温度设置: {device.Temperature}°C + 运行模式: {GetModeDescription(device.Mode)} + 风速: {GetFanSpeedDescription(device.FanSpeed)} + """, + _ => "未知设备类型" + }; + + return $""" + 设备信息 + 名称: {device.Name} + ID: {device.Id} + 类型: {device.Type} + + {details} + """; + } + + // ==================== 辅助方法 ==================== + + private string HandleCurtainOpen(DeviceState device) + { + device.Position = 100; + device.IsOn = true; + return $""" + 窗帘正在打开... + 设备: {device.Name} + 目标位置: 100% (完全打开) + 预计时间: 5秒 + """; + } + + private string HandleCurtainClose(DeviceState device) + { + device.Position = 0; + device.IsOn = false; + return $""" + 窗帘正在关闭... + 设备: {device.Name} + 目标位置: 0% (完全关闭) + 预计时间: 5秒 + """; + } + + private string HandleCurtainStop(DeviceState device) + { + return $""" + 窗帘已停止 + 设备: {device.Name} + 当前位置: {device.Position}% + """; + } + + private static string GetModeDescription(string? mode) => mode switch + { + "cool" => "制冷", + "heat" => "制热", + "fan" => "送风", + "dry" => "除湿", + "auto" => "自动", + _ => "未知" + }; + + private static string GetFanSpeedDescription(string? speed) => speed switch + { + "low" => "低速", + "medium" => "中速", + "high" => "高速", + "auto" => "自动", + _ => "未知" + }; +} + +// 设备状态数据模型 +internal class DeviceState +{ + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string Type { get; set; } = ""; + public bool IsOn { get; set; } + + // 灯光属性 + public int Brightness { get; set; } + public int Temperature { get; set; } + + // 窗帘属性 + public int Position { get; set; } + + // 空调属性 + public string? Mode { get; set; } + public string? FanSpeed { get; set; } +} \ No newline at end of file diff --git a/AspNetCoreMcpServer/Tools/SampleLlmTool.cs b/AspNetCoreMcpServer/Tools/SampleLlmTool.cs new file mode 100644 index 0000000..7d06ec8 --- /dev/null +++ b/AspNetCoreMcpServer/Tools/SampleLlmTool.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpServer.Tools; + +/// +/// This tool uses dependency injection and async method +/// +[McpServerToolType] +public sealed class SampleLlmTool +{ + [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] + public static async Task SampleLLM( + McpServer thisServer, + [Description("The prompt to send to the LLM")] string prompt, + [Description("Maximum number of tokens to generate")] int maxTokens, + CancellationToken cancellationToken) + { + ChatOptions options = new() + { + Instructions = "You are a helpful test server.", + MaxOutputTokens = maxTokens, + Temperature = 0.7f, + }; + + var samplingResponse = await thisServer.AsSamplingChatClient().GetResponseAsync(prompt, options, cancellationToken); + + return $"LLM sampling result: {samplingResponse}"; + } +} \ No newline at end of file diff --git a/AspNetCoreMcpServer/Tools/WeatherTools.cs b/AspNetCoreMcpServer/Tools/WeatherTools.cs new file mode 100644 index 0000000..e2ef8dc --- /dev/null +++ b/AspNetCoreMcpServer/Tools/WeatherTools.cs @@ -0,0 +1,366 @@ +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AspNetCoreMcpServer.Tools; + +[McpServerToolType] +public sealed class WeatherTools +{ + private readonly IHttpClientFactory _httpClientFactory; + private const string WeatherApiUrl = "http://10.1.57.124:8002/ledclock/weather.txt"; + + public WeatherTools(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + [McpServerTool, Description("获取今天的天气信息")] + [McpMeta("category", "weather")] + [McpMeta("dataSource", "local")] + public async Task GetTodayWeather() + { + try + { + var weatherData = await FetchWeatherDataAsync(); + + if (weatherData?.Data == null || weatherData.Data.Count == 0) + { + return "无法获取天气数据"; + } + + // 获取今天的日期 + var today = DateTime.Now.ToString("yyyy-MM-dd"); + + // 查找今天的天气数据 + var todayWeather = weatherData.Data.FirstOrDefault(d => d.Date == today); + + if (todayWeather == null) + { + // 如果没有今天的数据,返回第一天的数据 + todayWeather = weatherData.Data.First(); + return $""" + 今天天气信息 (数据日期: {todayWeather.Date}) + 地区: {weatherData.Province}/{weatherData.City} + 天气: {todayWeather.WeatherDescription} + 温度: {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C + 风向风力: {todayWeather.WindDescription} + + 注意: 未找到今日准确数据,显示的是最近日期的天气信息。 + """; + } + + return $""" + 今天天气信息 + 地区: {weatherData.Province}/{weatherData.City} + 日期: {todayWeather.Date} + 天气: {todayWeather.WeatherDescription} + 温度: {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C + 风向风力: {todayWeather.WindDescription} + + {GetWeatherAdvice(todayWeather)} + """; + } + catch (HttpRequestException ex) + { + return $"网络请求失败: {ex.Message}"; + } + catch (JsonException ex) + { + return $"数据解析失败: {ex.Message}"; + } + catch (Exception ex) + { + return $"获取天气信息时发生错误: {ex.Message}"; + } + } + + [McpServerTool, Description("获取未来几天的天气预报")] + [McpMeta("category", "weather")] + [McpMeta("dataSource", "local")] + public async Task GetWeatherForecast( + [Description("预报天数,默认为3天,最多15天")] int days = 3) + { + try + { + if (days < 1 || days > 15) + { + return "预报天数必须在1-15天之间"; + } + + var weatherData = await FetchWeatherDataAsync(); + + if (weatherData?.Data == null || weatherData.Data.Count == 0) + { + return "无法获取天气数据"; + } + + var forecasts = weatherData.Data.Take(days).Select((weather, index) => + { + var dateLabel = index == 0 ? "今天" : index == 1 ? "明天" : index == 2 ? "后天" : weather.Date; + return $""" + {dateLabel} ({weather.Date}) + 天气: {weather.WeatherDescription} + 温度: {weather.MinTemperature}°C ~ {weather.MaxTemperature}°C + 风向风力: {weather.WindDescription} + """; + }); + + return $""" + {weatherData.Province}/{weatherData.City} 未来{days}天天气预报 + + {string.Join("\n---\n", forecasts)} + """; + } + catch (Exception ex) + { + return $"获取天气预报时发生错误: {ex.Message}"; + } + } + + [McpServerTool, Description("获取指定日期的天气信息")] + [McpMeta("category", "weather")] + [McpMeta("dataSource", "local")] + public async Task GetWeatherByDate( + [Description("日期,格式: yyyy-MM-dd,例如: 2025-11-15")] string date) + { + try + { + // 验证日期格式 + if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, + System.Globalization.DateTimeStyles.None, out var parsedDate)) + { + return "日期格式错误,请使用 yyyy-MM-dd 格式,例如: 2025-11-15"; + } + + var weatherData = await FetchWeatherDataAsync(); + + if (weatherData?.Data == null || weatherData.Data.Count == 0) + { + return "无法获取天气数据"; + } + + var targetWeather = weatherData.Data.FirstOrDefault(d => d.Date == date); + + if (targetWeather == null) + { + var availableDates = string.Join(", ", weatherData.Data.Select(d => d.Date).Take(5)); + return $"未找到日期 {date} 的天气数据\n\n可用的日期: {availableDates}..."; + } + + var daysFromNow = (parsedDate - DateTime.Now).Days; + var dateLabel = daysFromNow switch + { + 0 => "今天", + 1 => "明天", + 2 => "后天", + _ => daysFromNow > 0 ? $"{daysFromNow}天后" : $"{-daysFromNow}天前" + }; + + return $""" + {dateLabel}的天气信息 + 地区: {weatherData.Province}/{weatherData.City} + 日期: {targetWeather.Date} + 天气: {targetWeather.WeatherDescription} + 温度: {targetWeather.MinTemperature}°C ~ {targetWeather.MaxTemperature}°C + 风向风力: {targetWeather.WindDescription} + + {GetWeatherAdvice(targetWeather)} + """; + } + catch (Exception ex) + { + return $"获取天气信息时发生错误: {ex.Message}"; + } + } + + [McpServerTool, Description("获取完整的天气数据概览")] + [McpMeta("category", "weather")] + [McpMeta("dataSource", "local")] + public async Task GetWeatherOverview() + { + try + { + var weatherData = await FetchWeatherDataAsync(); + + if (weatherData?.Data == null || weatherData.Data.Count == 0) + { + return "无法获取天气数据"; + } + + // 统计信息 + var totalDays = weatherData.Data.Count; + var avgMaxTemp = weatherData.Data.Average(d => int.Parse(d.MaxTemperature)); + var avgMinTemp = weatherData.Data.Average(d => int.Parse(d.MinTemperature)); + var rainyDays = weatherData.Data.Count(d => d.WeatherDescription.Contains("雨")); + var sunnyDays = weatherData.Data.Count(d => d.WeatherDescription.Contains("晴")); + + // 今天的天气 + var today = DateTime.Now.ToString("yyyy-MM-dd"); + var todayWeather = weatherData.Data.FirstOrDefault(d => d.Date == today) + ?? weatherData.Data.First(); + + return $""" + 天气数据概览 + 地区: {weatherData.Province}/{weatherData.City} + 数据范围: {weatherData.Data.First().Date} 至 {weatherData.Data.Last().Date} + + 今日天气: + {todayWeather.WeatherDescription} | {todayWeather.MinTemperature}°C ~ {todayWeather.MaxTemperature}°C | {todayWeather.WindDescription} + + 统计信息 (未来{totalDays}天): + - 平均最高温度: {avgMaxTemp:F1}°C + - 平均最低温度: {avgMinTemp:F1}°C + - 晴天: {sunnyDays}天 + - 雨天: {rainyDays}天 + + 温馨提示: {GetOverviewAdvice(weatherData.Data)} + """; + } + catch (Exception ex) + { + return $"获取天气概览时发生错误: {ex.Message}"; + } + } + + // ==================== 私有辅助方法 ==================== + + private async Task FetchWeatherDataAsync() + { + var client = _httpClientFactory.CreateClient(); + client.Timeout = TimeSpan.FromSeconds(10); + + using var response = await client.GetAsync(WeatherApiUrl); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + return JsonSerializer.Deserialize(content, options); + } + + private static string GetWeatherAdvice(WeatherData weather) + { + var advice = new List(); + + // 根据天气状况给建议 + if (weather.WeatherDescription.Contains("雨")) + { + advice.Add("☔ 记得带伞"); + } + else if (weather.WeatherDescription.Contains("晴")) + { + advice.Add("☀️ 天气晴朗,适合外出"); + } + else if (weather.WeatherDescription.Contains("多云")) + { + advice.Add("⛅ 天气多云,可以外出活动"); + } + + // 根据温度给建议 + var maxTemp = int.Parse(weather.MaxTemperature); + var minTemp = int.Parse(weather.MinTemperature); + var tempDiff = maxTemp - minTemp; + + if (maxTemp > 30) + { + advice.Add("🌡️ 高温天气,注意防暑降温"); + } + else if (minTemp < 10) + { + advice.Add("🧥 气温较低,注意保暖"); + } + + if (tempDiff > 10) + { + advice.Add("👔 昼夜温差大,适当增减衣物"); + } + + return advice.Count > 0 ? $"温馨提示: {string.Join(";", advice)}" : ""; + } + + private static string GetOverviewAdvice(List weatherList) + { + var adviceList = new List(); + + // 检查是否有连续雨天 + var consecutiveRainyDays = 0; + var maxConsecutiveRainy = 0; + foreach (var weather in weatherList) + { + if (weather.WeatherDescription.Contains("雨")) + { + consecutiveRainyDays++; + maxConsecutiveRainy = Math.Max(maxConsecutiveRainy, consecutiveRainyDays); + } + else + { + consecutiveRainyDays = 0; + } + } + + if (maxConsecutiveRainy >= 3) + { + adviceList.Add($"未来有连续{maxConsecutiveRainy}天降雨,注意出行安全"); + } + + // 检查温度变化 + var firstTemp = int.Parse(weatherList.First().MaxTemperature); + var lastTemp = int.Parse(weatherList.Last().MaxTemperature); + var tempChange = lastTemp - firstTemp; + + if (Math.Abs(tempChange) > 5) + { + adviceList.Add(tempChange > 0 ? "未来气温逐渐上升" : "未来气温逐渐下降"); + } + + return adviceList.Count > 0 ? string.Join(";", adviceList) : "天气状况平稳"; + } +} + +// ==================== 数据模型 ==================== + +internal class WeatherResponse +{ + [JsonPropertyName("code")] + public int Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = ""; + + [JsonPropertyName("province")] + public string Province { get; set; } = ""; + + [JsonPropertyName("city")] + public string City { get; set; } = ""; + + [JsonPropertyName("data")] + public List Data { get; set; } = new(); +} + +internal class WeatherData +{ + [JsonPropertyName("date")] + public string Date { get; set; } = ""; + + [JsonPropertyName("weatherDescription")] + public string WeatherDescription { get; set; } = ""; + + [JsonPropertyName("maxTemperature")] + public string MaxTemperature { get; set; } = ""; + + [JsonPropertyName("minTemperature")] + public string MinTemperature { get; set; } = ""; + + [JsonPropertyName("wea_img")] + public string WeaImg { get; set; } = ""; + + [JsonPropertyName("windDescription")] + public string WindDescription { get; set; } = ""; +} \ No newline at end of file diff --git a/AspNetCoreMcpServer/appsettings.Development.json b/AspNetCoreMcpServer/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/AspNetCoreMcpServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/AspNetCoreMcpServer/appsettings.json b/AspNetCoreMcpServer/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/AspNetCoreMcpServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ConsoleEchoMcpServer/ConsoleEchoMcpServer.csproj b/ConsoleEchoMcpServer/ConsoleEchoMcpServer.csproj index 0fd0cbb..39db7b6 100644 --- a/ConsoleEchoMcpServer/ConsoleEchoMcpServer.csproj +++ b/ConsoleEchoMcpServer/ConsoleEchoMcpServer.csproj @@ -2,12 +2,13 @@ Exe - net8.0 + net9.0 enable enable + diff --git a/ConsoleEchoMcpServer/Program.cs b/ConsoleEchoMcpServer/Program.cs index e5dff12..65c2849 100644 --- a/ConsoleEchoMcpServer/Program.cs +++ b/ConsoleEchoMcpServer/Program.cs @@ -1,3 +1,14 @@ -// See https://aka.ms/new-console-template for more information +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.ComponentModel; +using ConsoleEchoMcpServer.Tools; + +var builder = Host.CreateEmptyApplicationBuilder(settings: null); +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); + -Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/ConsoleEchoMcpServer/Tools/EchoTool.cs b/ConsoleEchoMcpServer/Tools/EchoTool.cs new file mode 100644 index 0000000..24673b1 --- /dev/null +++ b/ConsoleEchoMcpServer/Tools/EchoTool.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +namespace ConsoleEchoMcpServer.Tools; + +[McpServerToolType] +public sealed class EchoTool +{ + [McpServerTool, Description("Echoes the message back to the client.")] + public static string Echo(string message) => $"Hello from C#: {message}"; + + [McpServerTool, Description("Echoes in reverse the message sent.")] + public static string ReverseEcho(string message) => string.Concat(message.Reverse()); +} \ No newline at end of file diff --git a/McpServerCSharp.sln b/McpServerCSharp.sln index 1847fd2..3b7c021 100644 --- a/McpServerCSharp.sln +++ b/McpServerCSharp.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleEchoMcpServer", "ConsoleEchoMcpServer\ConsoleEchoMcpServer.csproj", "{D9A9A1BA-D847-4083-A085-DCBD43B5E1DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreMcpServer", "AspNetCoreMcpServer\AspNetCoreMcpServer.csproj", "{6001BAA3-90C0-4C77-BEEF-0CF788E7898E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {D9A9A1BA-D847-4083-A085-DCBD43B5E1DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9A9A1BA-D847-4083-A085-DCBD43B5E1DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9A9A1BA-D847-4083-A085-DCBD43B5E1DC}.Release|Any CPU.Build.0 = Release|Any CPU + {6001BAA3-90C0-4C77-BEEF-0CF788E7898E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6001BAA3-90C0-4C77-BEEF-0CF788E7898E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6001BAA3-90C0-4C77-BEEF-0CF788E7898E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6001BAA3-90C0-4C77-BEEF-0CF788E7898E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal