diff options
Diffstat (limited to 'src/Agent.Mcp/McpHost.cs')
| -rw-r--r-- | src/Agent.Mcp/McpHost.cs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/src/Agent.Mcp/McpHost.cs b/src/Agent.Mcp/McpHost.cs new file mode 100644 index 0000000..ff546e0 --- /dev/null +++ b/src/Agent.Mcp/McpHost.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Nodes; +using System.Reflection; +using Agent.Core.Localization; +using Agent.Core.Tasks; +using Agent.Core.Ollama; +using Agent.Core.Runtime; +using Agent.Core.Tools; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using ModelContextProtocol.Server; + +namespace Agent.Mcp; + +public static class McpHost +{ + public static async Task RunStdioAsync(LocaleCatalog locale, OllamaSettings ollamaSettings, AgentRuntimeSettings runtimeSettings, VisionLimits visionLimits, CancellationToken cancellationToken, InternalToolDispatcher? toolDispatcher = null, IAgentRuntimeObserver? observer = null) + { + HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings + { + Args = [], + ContentRootPath = AppContext.BaseDirectory + }); + builder.Logging.ClearProviders(); + var registry = new TaskRegistry(); + using var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; + var ollama = new OllamaClient(httpClient, ollamaSettings); + var runtime = new AgentRuntime(registry, ollama, runtimeSettings, toolDispatcher, observer); + builder.Services.AddSingleton(locale); + builder.Services.AddSingleton(registry); + builder.Services.AddSingleton(runtime); + builder.Services.AddHostedService<QueuedTaskWorker>(); + + McpServerTool[] tools = CreateTools(registry, locale, visionLimits); + + builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(tools); + + using IHost host = builder.Build(); + await host.RunAsync(cancellationToken).ConfigureAwait(false); + } + + public static async Task RunHttpAsync(LocaleCatalog locale, OllamaSettings ollamaSettings, AgentRuntimeSettings runtimeSettings, VisionLimits visionLimits, string hostName, int port, CancellationToken cancellationToken, InternalToolDispatcher? toolDispatcher = null, IAgentRuntimeObserver? observer = null) + { + if (!System.Net.IPAddress.TryParse(hostName, out System.Net.IPAddress? address) || !System.Net.IPAddress.IsLoopback(address)) + throw new ArgumentException("HTTP transport host must be a loopback IP literal.", nameof(hostName)); + WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [], ContentRootPath = AppContext.BaseDirectory }); + builder.Logging.ClearProviders(); + string listenHost = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ? $"[{address}]" : address.ToString(); + builder.WebHost.UseUrls($"http://{listenHost}:{port}"); + var registry = new TaskRegistry(); + var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; + var runtime = new AgentRuntime(registry, new OllamaClient(httpClient, ollamaSettings), runtimeSettings, toolDispatcher, observer); + builder.Services.AddSingleton(locale); + builder.Services.AddSingleton(registry); + builder.Services.AddSingleton(runtime); + builder.Services.AddHostedService<QueuedTaskWorker>(); + McpServerTool[] tools = CreateTools(registry, locale, visionLimits); + builder.Services.AddMcpServer().WithHttpTransport().WithTools(tools); + await using WebApplication app = builder.Build(); + app.Use(async (context, next) => + { + if (context.Request.Headers.TryGetValue("Origin", out Microsoft.Extensions.Primitives.StringValues origins) && + origins.Any(origin => !IsLoopbackOrigin(origin))) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + if (context.Request.Path == "/mcp" && HttpMethods.IsGet(context.Request.Method)) + { + context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; + return; + } + await next(context).ConfigureAwait(false); + }); + app.MapMcp("/mcp"); + try { await app.RunAsync(cancellationToken).ConfigureAwait(false); } + finally { httpClient.Dispose(); } + } + + private static bool IsLoopbackOrigin(string? origin) + { + return Uri.TryCreate(origin, UriKind.Absolute, out Uri? uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) && + (string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase) || + (System.Net.IPAddress.TryParse(uri.Host, out System.Net.IPAddress? address) && System.Net.IPAddress.IsLoopback(address))); + } + + private static McpServerTool[] CreateTools(TaskRegistry registry, LocaleCatalog locale, VisionLimits visionLimits) + { + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow + }; + var target = new AgentTools(registry, locale, visionLimits); + McpServerTool[] tools = typeof(AgentTools) + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .Where(static method => method.GetCustomAttribute<McpServerToolAttribute>() is not null) + .Select(method => McpServerTool.Create(method, target, new McpServerToolCreateOptions { SerializerOptions = serializerOptions })) + .ToArray(); + foreach (McpServerTool tool in tools) MakeSchemaStrict(tool); + return tools; + } + + private static void MakeSchemaStrict(McpServerTool tool) + { + JsonNode root = JsonNode.Parse(tool.ProtocolTool.InputSchema.GetRawText()) + ?? throw new InvalidOperationException($"Tool '{tool.ProtocolTool.Name}' has no input schema."); + Visit(root); + tool.ProtocolTool.InputSchema = JsonSerializer.SerializeToElement(root); + + static void Visit(JsonNode? node) + { + if (node is JsonObject obj) + { + if (obj["type"] is JsonValue type && type.TryGetValue(out string? typeName) && typeName == "object") + obj["additionalProperties"] = false; + foreach (KeyValuePair<string, JsonNode?> property in obj.ToArray()) Visit(property.Value); + } + else if (node is JsonArray array) + { + foreach (JsonNode? item in array) Visit(item); + } + } + } +} |
