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(); 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(); 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() 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 property in obj.ToArray()) Visit(property.Value); } else if (node is JsonArray array) { foreach (JsonNode? item in array) Visit(item); } } } }