From f13d3c99237b86d527438ed6dcb4bc2818415644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Tue, 6 May 2025 17:04:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lazy52API.sln | 25 +++ .../McpEndpointRouteBuilderExtensions.cs | 156 ++++++++++++++++++ lazy52API/Program.cs | 30 ++++ lazy52API/Properties/launchSettings.json | 52 ++++++ lazy52API/Tool/doubao.cs | 53 ++++++ lazy52API/appsettings.Development.json | 8 + lazy52API/appsettings.json | 16 ++ lazy52API/lazy52API.csproj | 31 ++++ 8 files changed, 371 insertions(+) create mode 100644 lazy52API.sln create mode 100644 lazy52API/McpEndpointRouteBuilderExtensions.cs create mode 100644 lazy52API/Program.cs create mode 100644 lazy52API/Properties/launchSettings.json create mode 100644 lazy52API/Tool/doubao.cs create mode 100644 lazy52API/appsettings.Development.json create mode 100644 lazy52API/appsettings.json create mode 100644 lazy52API/lazy52API.csproj diff --git a/lazy52API.sln b/lazy52API.sln new file mode 100644 index 0000000..08767b3 --- /dev/null +++ b/lazy52API.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "lazy52API", "lazy52API\lazy52API.csproj", "{13E3BF0F-6CBC-465C-A566-FB635D87DCA9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13E3BF0F-6CBC-465C-A566-FB635D87DCA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E3BF0F-6CBC-465C-A566-FB635D87DCA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E3BF0F-6CBC-465C-A566-FB635D87DCA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E3BF0F-6CBC-465C-A566-FB635D87DCA9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0CA45D20-65DD-42F9-9F9D-382FA58B5226} + EndGlobalSection +EndGlobal diff --git a/lazy52API/McpEndpointRouteBuilderExtensions.cs b/lazy52API/McpEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..4170b21 --- /dev/null +++ b/lazy52API/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/lazy52API/Program.cs b/lazy52API/Program.cs new file mode 100644 index 0000000..f9248a2 --- /dev/null +++ b/lazy52API/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/lazy52API/Properties/launchSettings.json b/lazy52API/Properties/launchSettings.json new file mode 100644 index 0000000..78a1005 --- /dev/null +++ b/lazy52API/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5010" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7110;http://localhost:5010" + }, + "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:2486", + "sslPort": 44340 + } + } +} \ No newline at end of file diff --git a/lazy52API/Tool/doubao.cs b/lazy52API/Tool/doubao.cs new file mode 100644 index 0000000..c4572b4 --- /dev/null +++ b/lazy52API/Tool/doubao.cs @@ -0,0 +1,53 @@ +using Flurl; +using Flurl.Http; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace lazy52API.Tool +{ + [McpServerToolType] + public static class Painting + { + /// + /// 调用豆包绘图接口 + /// + /// + [McpServerTool(Name = "DouBaoPainting"), Description("调用豆包绘图接口;输出image_url数组(一般是四个)")] + public static string DouBao( + [Description("要生成图片的描述")] string description, + [Description("绘画风格")] string type, + [Description("绘画比例,可选9:16,2:3,3:4,4:3,1:1,3:2,16:9。不填默认16:9")] string ratio = "16:9" + ) + { + Console.WriteLine($"接收到绘图任务:{description}"); + var Painting = new + { + description, + type, + ratio + }; + var filteredPainting = new System.Collections.Generic.Dictionary(); + foreach (var prop in Painting.GetType().GetProperties()) + { + var value = prop.GetValue(Painting); + if (value != null) + { + filteredPainting[prop.Name] = value; + } + } + var request = new FlurlRequest("https://npi.lazy52.com/api/doubao") + .WithTimeout(Timeout.InfiniteTimeSpan); + var response = request + .SetQueryParams(filteredPainting).GetAsync().GetAwaiter().GetResult(); + if (response != null) + { + var responseString = response.GetStringAsync().GetAwaiter().GetResult(); + //JToken rootToken = JToken.Parse(responseString); + //var resultPath = "$.image_url[*]"; + //IEnumerable resultTokens = rootToken.SelectTokens(resultPath); + return responseString; + } + return ""; + } + } +} diff --git a/lazy52API/appsettings.Development.json b/lazy52API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/lazy52API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/lazy52API/appsettings.json b/lazy52API/appsettings.json new file mode 100644 index 0000000..4009aa3 --- /dev/null +++ b/lazy52API/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:9090" // + } + } + } +} diff --git a/lazy52API/lazy52API.csproj b/lazy52API/lazy52API.csproj new file mode 100644 index 0000000..31dc20a --- /dev/null +++ b/lazy52API/lazy52API.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + linux-x64 + linux-x64 + True + mcr.microsoft.com/dotnet/aspnet:8.0 + e1ff1491-e3f4-4af6-822c-2ca2d8aeb211 + + + + + + + + + + + + + + + + + + + +