summaryrefslogtreecommitdiff
path: root/src/Agent.Mcp/McpHost.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Mcp/McpHost.cs')
-rw-r--r--src/Agent.Mcp/McpHost.cs136
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);
+ }
+ }
+ }
+}