diff --git a/GLMSearch.sln b/GLMSearch.sln new file mode 100644 index 0000000..1c86f98 --- /dev/null +++ b/GLMSearch.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35728.132 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GLMSearch", "GLMSearch\GLMSearch.csproj", "{3FD357C4-E366-4F59-8953-4218B4AAAED6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3FD357C4-E366-4F59-8953-4218B4AAAED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FD357C4-E366-4F59-8953-4218B4AAAED6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FD357C4-E366-4F59-8953-4218B4AAAED6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FD357C4-E366-4F59-8953-4218B4AAAED6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/GLMSearch/.config/dotnet-tools.json b/GLMSearch/.config/dotnet-tools.json new file mode 100644 index 0000000..0280531 --- /dev/null +++ b/GLMSearch/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.3", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/GLMSearch/GLMSearch.csproj b/GLMSearch/GLMSearch.csproj new file mode 100644 index 0000000..471418a --- /dev/null +++ b/GLMSearch/GLMSearch.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + linux-x64 + True + mcr.microsoft.com/dotnet/aspnet:8.0 + d0a7be0b-f5c4-418d-ad02-04b2f1ce479b + + + + + + + + + + + + + + + + + + + + diff --git a/GLMSearch/GLMSearch.http b/GLMSearch/GLMSearch.http new file mode 100644 index 0000000..7567dcf --- /dev/null +++ b/GLMSearch/GLMSearch.http @@ -0,0 +1,6 @@ +@GLMSearch_HostAddress = http://localhost:5085 + +GET {{GLMSearch_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/GLMSearch/McpEndpointRouteBuilderExtensions.cs b/GLMSearch/McpEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..4170b21 --- /dev/null +++ b/GLMSearch/McpEndpointRouteBuilderExtensions.cs @@ -0,0 +1,156 @@ +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol.Messages; +using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Server; +using ModelContextProtocol.Utils.Json; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +namespace WebSSE +{ + /// + /// Extension methods for to add MCP endpoints. + /// https://github.com/modelcontextprotocol/csharp-sdk/tree/main + /// + public static class McpEndpointRouteBuilderExtensions + { + /// + /// Sets up endpoints for handling MCP HTTP Streaming transport. + /// + /// The web application to attach MCP HTTP endpoints. + /// The route pattern prefix to map to. + /// Configure per-session options. + /// Provides an optional asynchronous callback for handling new MCP sessions. + /// Returns a builder for configuring additional endpoint conventions like authorization policies. + public static IEndpointConventionBuilder MapMcp( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern = "", + Func? configureOptionsAsync = null, + Func? runSessionAsync = null) + => endpoints.MapMcp(RoutePatternFactory.Parse(pattern), configureOptionsAsync, runSessionAsync); + + /// + /// Sets up endpoints for handling MCP HTTP Streaming transport. + /// + /// The web application to attach MCP HTTP endpoints. + /// The route pattern prefix to map to. + /// Configure per-session options. + /// Provides an optional asynchronous callback for handling new MCP sessions. + /// Returns a builder for configuring additional endpoint conventions like authorization policies. + public static IEndpointConventionBuilder MapMcp(this IEndpointRouteBuilder endpoints, + RoutePattern pattern, + Func? configureOptionsAsync = null, + Func? runSessionAsync = null) + { + ConcurrentDictionary _sessions = new(StringComparer.Ordinal); + + var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); + var optionsSnapshot = endpoints.ServiceProvider.GetRequiredService>(); + var optionsFactory = endpoints.ServiceProvider.GetRequiredService>(); + var hostApplicationLifetime = endpoints.ServiceProvider.GetRequiredService(); + + var routeGroup = endpoints.MapGroup(pattern); + + routeGroup.MapGet("/sse", async context => + { + // If the server is shutting down, we need to cancel all SSE connections immediately without waiting for HostOptions.ShutdownTimeout + // which defaults to 30 seconds. + using var sseCts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted, hostApplicationLifetime.ApplicationStopping); + var cancellationToken = sseCts.Token; + + var response = context.Response; + response.Headers.ContentType = "text/event-stream"; + response.Headers.CacheControl = "no-cache,no-store"; + + // Make sure we disable all response buffering for SSE + context.Response.Headers.ContentEncoding = "identity"; + context.Features.GetRequiredFeature().DisableBuffering(); + + var sessionId = MakeNewSessionId(); + await using var transport = new SseResponseStreamTransport(response.Body, $"/message?sessionId={sessionId}"); + if (!_sessions.TryAdd(sessionId, transport)) + { + throw new Exception($"Unreachable given good entropy! Session with ID '{sessionId}' has already been created."); + } + + var options = optionsSnapshot.Value; + if (configureOptionsAsync is not null) + { + options = optionsFactory.Create(Options.DefaultName); + await configureOptionsAsync.Invoke(context, options, cancellationToken); + } + + try + { + var transportTask = transport.RunAsync(cancellationToken); + + try + { + await using var mcpServer = McpServerFactory.Create(transport, options, loggerFactory, endpoints.ServiceProvider); + context.Features.Set(mcpServer); + + runSessionAsync ??= RunSession; + await runSessionAsync(context, mcpServer, cancellationToken); + } + finally + { + await transport.DisposeAsync(); + await transportTask; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // RequestAborted always triggers when the client disconnects before a complete response body is written, + // but this is how SSE connections are typically closed. + } + finally + { + _sessions.TryRemove(sessionId, out _); + } + }); + + routeGroup.MapPost("/message", async context => + { + if (!context.Request.Query.TryGetValue("sessionId", out var sessionId)) + { + await Results.BadRequest("Missing sessionId query parameter.").ExecuteAsync(context); + return; + } + + if (!_sessions.TryGetValue(sessionId.ToString(), out var transport)) + { + await Results.BadRequest($"Session ID not found.").ExecuteAsync(context); + return; + } + + var message = (IJsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), context.RequestAborted); + if (message is null) + { + await Results.BadRequest("No message in request body.").ExecuteAsync(context); + return; + } + + await transport.OnMessageReceivedAsync(message, context.RequestAborted); + context.Response.StatusCode = StatusCodes.Status202Accepted; + await context.Response.WriteAsync("Accepted"); + }); + + return routeGroup; + } + + private static Task RunSession(HttpContext httpContext, IMcpServer session, CancellationToken requestAborted) + => session.RunAsync(requestAborted); + + private static string MakeNewSessionId() + { + // 128 bits + Span buffer = stackalloc byte[16]; + RandomNumberGenerator.Fill(buffer); + return WebEncoders.Base64UrlEncode(buffer); + } + } +} diff --git a/GLMSearch/Program.cs b/GLMSearch/Program.cs new file mode 100644 index 0000000..a466542 --- /dev/null +++ b/GLMSearch/Program.cs @@ -0,0 +1,30 @@ +using WebSSE; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddMcpServer().WithToolsFromAssembly(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); +//app.MapMcpSse(); +app.MapMcp(); + +app.Run(); diff --git a/GLMSearch/Properties/launchSettings.json b/GLMSearch/Properties/launchSettings.json new file mode 100644 index 0000000..6713e29 --- /dev/null +++ b/GLMSearch/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5085" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7099;http://localhost:5085" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (.NET SDK)": { + "commandName": "SdkContainer", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:34419", + "sslPort": 44383 + } + } +} \ No newline at end of file diff --git a/GLMSearch/Tool/Search.cs b/GLMSearch/Tool/Search.cs new file mode 100644 index 0000000..7061b04 --- /dev/null +++ b/GLMSearch/Tool/Search.cs @@ -0,0 +1,56 @@ +using Flurl.Http; +using ModelContextProtocol.Server; +using Newtonsoft.Json.Linq; +using System.ComponentModel; + +namespace GLMSearch.Tools +{ + [McpServerToolType] + public static class GLMWebSearch + { + [McpServerTool(Name = "WebSearch"), Description("通过关键字进行Web搜索")] + public static string WebSearch( + [Description("搜索关键字")] string keyword, + [Description("搜索引擎,请务必从[search_std,search_pro,search_pro_quark]中选择一个,默认是search_pro_quark,当返回失败或检索内容过少时可以尝试切换其他搜索引擎")] string search_engine = "search_pro_quark") + { + Console.WriteLine($"接收到搜索任务:{keyword}"); + //判断search_engine是否在 search_std search_pro search_pro_sogou search_pro_quark search_pro_jina + if (!new string[] { "search_std", "search_pro", "search_pro_sogou", "search_pro_quark", "search_pro_jina" }.Contains(search_engine)) + { + search_engine = "search_std"; + } + //https://bigmodel.cn/dev/api/search-tool/web-search + var response = "https://open.bigmodel.cn/api/paas/v4/web_search" + .WithHeader("accept", "*/*") + .WithHeader("Content-Type", "application/json") + .WithHeader("Authorization", "Bearer b7a76cdc214a6956c4de678230ef5814.PumUMS1MJ51ji4aE") + .PostJsonAsync(new + { + search_engine = search_engine,//search_std search_pro search_pro_sogou search_pro_quark search_pro_jina + request_id = new Guid().ToString(), + search_query = keyword + }) + .GetAwaiter().GetResult(); + // 处理响应 + var responseString = response.GetStringAsync().GetAwaiter().GetResult(); + Console.WriteLine($"检索完成:{keyword}"); + JToken rootToken = JToken.Parse(responseString); + var resultPath = "$.search_result"; + IEnumerable resultTokens = rootToken.SelectTokens(resultPath); + var newresultObjects = resultTokens.First().Select(result => + { + // For each book JToken, create a new JObject + return new JObject( + new JProperty("content", result["content"]), + new JProperty("title", result["title"]), + new JProperty("media", result["media"]), + new JProperty("link", result["link"]) + ); + }).ToList(); + + var res = JToken.FromObject(newresultObjects).ToString(formatting: Newtonsoft.Json.Formatting.None); + Console.WriteLine($"检索返回:{res}"); + return res; + } + } +} diff --git a/GLMSearch/Tool/Speech.cs b/GLMSearch/Tool/Speech.cs new file mode 100644 index 0000000..2807be3 --- /dev/null +++ b/GLMSearch/Tool/Speech.cs @@ -0,0 +1,77 @@ +using Flurl.Http; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +//[McpServerToolType] +public class Speech +{ + //[McpServerTool(Name = "GenerateSpeech"), + // Description("生成语音,并返回链接;在返回后应该输出一个 html video标签,以播放语音;")] + public static string GenerateSpeech( + [Description("The text to convert to speech"), Required] string input, + [Description("The voice to use (see Available Voices); Available options: alloy(科技感中性音), ash(低沉烟嗓), ballad(温暖民谣音), coral(活泼少女音), echo(空间混响音), fable(智者老年音), onyx(权威男声), nova(清澈青年音), sage(温和中性音), shimmer(梦幻女声), verse(韵律朗诵音). Default: alloy "), Required] string voice = "alloy", + [Description("Can be used to guide voice emotion or style"), Required] string instructions = null, + [Description("The format of the audio response. Supported formats: mp3, opus, aac, flac, wav, pcm. Defaults to mp3.")] + string responseFormat = "wav") + { + var payload = new + { + input, + voice, + prompt = instructions, // Mapped to "prompt" parameter in API + response_format = responseFormat + }; + var filteredPayload = new System.Collections.Generic.Dictionary(); + foreach (var prop in payload.GetType().GetProperties()) + { + var value = prop.GetValue(payload); + if (value != null) + { + filteredPayload[prop.Name] = value; + } + } + var response = "https://ttsapi.site/v1/audio/speech" + .PostJsonAsync(filteredPayload).GetAwaiter().GetResult(); + if (response != null) + { + // 1. 获取应用程序基目录(适用于控制台/Windows服务) + string baseDir = AppContext.BaseDirectory; + + // 2. 向上查找直到找到项目根目录(根据实际项目结构调整) + var projectRoot = FindProjectRoot(baseDir); + + // 3. 创建tts目录(放在项目根目录下的wwwroot/tts中) + string ttsDir = Path.Combine(projectRoot, "wwwroot", "tts"); + Directory.CreateDirectory(ttsDir); + // 4. 生成文件名和路径 + string fileName = $"{Guid.NewGuid()}.{responseFormat}"; + string filePath = Path.Combine(ttsDir, fileName); + using (var stream = response.GetStreamAsync().GetAwaiter().GetResult()) + using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + stream.CopyTo(fileStream); + } + string publicUrl = $"https://search.mcp.shizhuoran.top/tts/{fileName}"; + return publicUrl; + } + return ""; + } + + private static string FindProjectRoot(string startPath) + { + var directory = new DirectoryInfo(startPath); + + // 向上查找直到找到包含.csproj文件的目录 + while (directory != null) + { + if (Directory.GetFiles(directory.FullName, "*.csproj").Length > 0) + { + return directory.FullName; + } + directory = directory.Parent; + } + + // 如果找不到,返回原始路径 + return startPath; + } +} diff --git a/GLMSearch/Tool/Speech2.cs b/GLMSearch/Tool/Speech2.cs new file mode 100644 index 0000000..770a39f --- /dev/null +++ b/GLMSearch/Tool/Speech2.cs @@ -0,0 +1,83 @@ +using Flurl; +using Flurl.Http; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace GLMSearch.Tool +{ + [McpServerToolType] + public class Speech2 + { + //SpeechGenerate + [McpServerTool(Name = "SpeechGenerate"), + Description("生成语音,并返回链接;在返回后应该输出一个 markdown 超链接 ,以播放语音;")] + public static string SpeechGenerate( + [Description("需要转换的文本,注意 这里传入的是播报的文本,请输入常规的文本格式而不是富文本;最终的语音播报会是这个文本所以需要注意不要传入富文本或无法播放的符号等"), Required] string input, + [Description("The voice to use (see Available Voices); 请务必从以下语音中选择: [\r\n \"zh-CN-XiaoxiaoNeural\",\r\n \"zh-CN-YunxiNeural\",\r\n \"zh-CN-YunjianNeural\",\r\n \"zh-CN-XiaoyiNeural\",\r\n \"zh-CN-YunyangNeural\",\r\n \"zh-CN-XiaochenNeural\",\r\n \"zh-CN-XiaochenMultilingualNeural\",\r\n \"zh-CN-XiaohanNeural\",\r\n \"zh-CN-XiaomengNeural\",\r\n \"zh-CN-XiaomoNeural\",\r\n \"zh-CN-XiaoqiuNeural\",\r\n \"zh-CN-XiaorouNeural\",\r\n \"zh-CN-XiaoruiNeural\",\r\n \"zh-CN-XiaoshuangNeural\",\r\n \"zh-CN-XiaoxiaoDialectsNeural\",\r\n \"zh-CN-XiaoxiaoMultilingualNeural\",\r\n \"zh-CN-XiaoyanNeural\",\r\n \"zh-CN-XiaoyouNeural\",\r\n \"zh-CN-XiaoyuMultilingualNeural\",\r\n \"zh-CN-XiaozhenNeural\",\r\n \"zh-CN-YunfengNeural\",\r\n \"zh-CN-YunhaoNeural\",\r\n \"zh-CN-YunjieNeural\",\r\n \"zh-CN-YunxiaNeural\",\r\n \"zh-CN-YunyeNeural\",\r\n \"zh-CN-YunyiMultilingualNeural\",\r\n \"zh-CN-YunzeNeural\",\r\n \"zh-CN-YunfanMultilingualNeural\",\r\n \"zh-CN-YunxiaoMultilingualNeural\",\r\n \"zh-CN-guangxi-YunqiNeural\",\r\n \"zh-CN-henan-YundengNeural\",\r\n \"zh-CN-liaoning-XiaobeiNeural\",\r\n \"zh-CN-liaoning-YunbiaoNeural\",\r\n \"zh-CN-shaanxi-XiaoniNeural\",\r\n \"zh-CN-shandong-YunxiangNeural\",\r\n \"zh-CN-sichuan-YunxiNeural\"\r\n]\r\n. Default: zh-CN-XiaoxiaoNeural "), Required] string voice = "zh-CN-XiaoxiaoNeural", + [Description("Can be used to guide voice emotion or style;请务必从以下选择:[\r\n \"advertisement-upbeat\",\r\n \"affectionate\",\r\n \"angry\",\r\n \"assistant\",\r\n \"calm\",\r\n \"chat\",\r\n \"chat-casual\",\r\n \"cheerful\",\r\n \"customerservice\",\r\n \"depressed\",\r\n \"disgruntled\",\r\n \"documentary-narration\",\r\n \"embarrassed\",\r\n \"empathetic\",\r\n \"envious\",\r\n \"excited\",\r\n \"fearful\",\r\n \"friendly\",\r\n \"gentle\",\r\n \"livecommercial\",\r\n \"lyrical\",\r\n \"narration-professional\",\r\n \"narration-relaxed\",\r\n \"newscast\",\r\n \"newscast-casual\",\r\n \"poetry-reading\",\r\n \"sad\",\r\n \"serious\",\r\n \"sorry\",\r\n \"sports-commentary\",\r\n \"sports-commentary-excited\",\r\n \"story\",\r\n \"whispering\"\r\n]\r\n Default: newscast"), Required] string instructions = "newscast", + [Description("The format of the audio response. Supported formats: audio-16khz-32kbitrate-mono-mp3 \tMP3格式,16kHz, 32kbps\r\naudio-16khz-64kbitrate-mono-mp3 \tMP3格式,16kHz, 64kbps\r\naudio-16khz-128kbitrate-mono-mp3 \tMP3格式,16kHz, 128kbps\r\naudio-24khz-48kbitrate-mono-mp3 \tMP3格式,24kHz, 48kbps\r\naudio-24khz-96kbitrate-mono-mp3 \tMP3格式,24kHz, 96kbps\r\naudio-24khz-160kbitrate-mono-mp3 \tMP3格式,24kHz, 160kbps\r\nriff-16khz-16bit-mono-pcm \tWAV格式,16kHz\r\nriff-24khz-16bit-mono-pcm \tWAV格式,24kHz. Defaults to audio-16khz-32kbitrate-mono-mp3.")] + string responseFormat = "audio-16khz-32kbitrate-mono-mp3") + { + var payload = new + { + t = input,//要转换的文本(需要进行URL编码) + v = voice, + s = instructions, // Mapped to "prompt" parameter in API + o = responseFormat + }; + var filteredPayload = new System.Collections.Generic.Dictionary(); + foreach (var prop in payload.GetType().GetProperties()) + { + var value = prop.GetValue(payload); + if (value != null) + { + filteredPayload[prop.Name] = value; + } + } + var response = "http://148.135.77.70:8010/tts" + .SetQueryParams(filteredPayload).GetAsync().GetAwaiter().GetResult(); + if (response != null) + { + // 1. 获取应用程序基目录(适用于控制台/Windows服务) + string baseDir = AppContext.BaseDirectory; + + // 2. 向上查找直到找到项目根目录(根据实际项目结构调整) + var projectRoot = FindProjectRoot(baseDir); + + // 3. 创建tts目录(放在项目根目录下的wwwroot/tts中) + string ttsDir = Path.Combine(projectRoot, "wwwroot", "tts"); + Directory.CreateDirectory(ttsDir); + // 4. 生成文件名和路径 + string fileName = $"{Guid.NewGuid()}.mp3"; + string filePath = Path.Combine(ttsDir, fileName); + using (var stream = response.GetStreamAsync().GetAwaiter().GetResult()) + using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + stream.CopyTo(fileStream); + } + string publicUrl = $"https://search.mcp.shizhuoran.top/tts/{fileName}"; + return publicUrl; + } + return ""; + } + + private static string FindProjectRoot(string startPath) + { + var directory = new DirectoryInfo(startPath); + + // 向上查找直到找到包含.csproj文件的目录 + while (directory != null) + { + if (Directory.GetFiles(directory.FullName, "*.csproj").Length > 0) + { + return directory.FullName; + } + directory = directory.Parent; + } + + // 如果找不到,返回原始路径 + return startPath; + } + } +} diff --git a/GLMSearch/appsettings.Development.json b/GLMSearch/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/GLMSearch/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/GLMSearch/appsettings.json b/GLMSearch/appsettings.json new file mode 100644 index 0000000..4009aa3 --- /dev/null +++ b/GLMSearch/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:9090" // + } + } + } +} diff --git a/SearchApp/Program.cs b/SearchApp/Program.cs new file mode 100644 index 0000000..47bad91 --- /dev/null +++ b/SearchApp/Program.cs @@ -0,0 +1,11 @@ +using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Server; + +await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), + new() + { + ServerInfo = new() { Name = "MyServer", Version = "1.0.0" }, + Capabilities = new() { Tools =} + }); + +Console.WriteLine("Hello, World!"); diff --git a/SearchApp/SearchApp.csproj b/SearchApp/SearchApp.csproj new file mode 100644 index 0000000..69176b5 --- /dev/null +++ b/SearchApp/SearchApp.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + diff --git a/Tools/Search.cs b/Tools/Search.cs new file mode 100644 index 0000000..6e0ada2 --- /dev/null +++ b/Tools/Search.cs @@ -0,0 +1,54 @@ +using Flurl.Http; +using ModelContextProtocol.Server; +using Newtonsoft.Json.Linq; +using System.ComponentModel; + +namespace GLMSearch.Tools +{ + [McpServerToolType] + public static class GLMWebSearch + { + [McpServerTool(Name = "WebSearch"), Description("通过关键字进行Web搜索")] + public static string WebSearch( + [Description("搜索关键字")] string keyword) + { + Console.WriteLine($"接收到搜索任务:{keyword}"); + var response = "https://open.bigmodel.cn/api/paas/v4/tools" + .WithHeader("accept", "*/*") + .WithHeader("Content-Type", "application/json") + .WithHeader("Authorization", "Bearer b7a76cdc214a6956c4de678230ef5814.PumUMS1MJ51ji4aE") + .PostJsonAsync(new + { + tool = "web-search-pro", + request_id = new Guid().ToString(), + stream = false, + messages = new object[1] + { new { + role = "user", + content = keyword + } } + }) + .GetAwaiter().GetResult(); + // 处理响应 + var responseString = response.GetStringAsync().GetAwaiter().GetResult(); + Console.WriteLine($"检索完成:{keyword}"); + JToken rootToken = JToken.Parse(responseString); + var resultPath = "$.choices[0].message.tool_calls[?(@.search_result)].search_result"; + IEnumerable resultTokens = rootToken.SelectTokens(resultPath); + var newresultObjects = resultTokens.First().Select(result => + { + // For each book JToken, create a new JObject + return new JObject( + new JProperty("content", result["content"]), + new JProperty("title", result["title"]), + new JProperty("media", result["media"]), + new JProperty("link", result["link"]) + ); + }).ToList(); + + var res = JToken.FromObject(newresultObjects).ToString(formatting: Newtonsoft.Json.Formatting.None); + Console.WriteLine($"检索返回:{res}"); + return res; + } + } +} diff --git a/Tools/Tools.csproj b/Tools/Tools.csproj new file mode 100644 index 0000000..d7b669b --- /dev/null +++ b/Tools/Tools.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + +