diff options
Diffstat (limited to 'src/Agent.Mcp/McpHost.cs')
| -rw-r--r-- | src/Agent.Mcp/McpHost.cs | 93 |
1 files changed, 75 insertions, 18 deletions
diff --git a/src/Agent.Mcp/McpHost.cs b/src/Agent.Mcp/McpHost.cs index ff546e0..9a2ba66 100644 --- a/src/Agent.Mcp/McpHost.cs +++ b/src/Agent.Mcp/McpHost.cs @@ -15,12 +15,16 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using ModelContextProtocol.Server; +using ModelContextProtocol.Extensions.Tasks; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using System.Diagnostics; 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) + public static async Task RunStdioAsync(LocaleCatalog locale, OllamaSettings ollamaSettings, AgentRuntimeSettings runtimeSettings, VisionLimits visionLimits, CancellationToken cancellationToken, InternalToolDispatcher? toolDispatcher = null, IAgentRuntimeObserver? observer = null, McpTaskStoreSettings? taskStoreSettings = null, IMcpHostObserver? hostObserver = null, IReadOnlyList<McpServerTool>? additionalTools = null, IReadOnlySet<string>? taskCapableTools = null, IReadOnlySet<string>? requiredTaskTools = null, Action<IMcpServerBuilder>? configureBuilder = null) { HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { @@ -37,18 +41,24 @@ public static class McpHost builder.Services.AddSingleton(runtime); builder.Services.AddHostedService<QueuedTaskWorker>(); - McpServerTool[] tools = CreateTools(registry, locale, visionLimits); + var taskStore = new PersistentMcpTaskStore(taskStoreSettings ?? DefaultTaskStoreSettings()); + McpServerTool[] tools = CreateTools(registry, locale, visionLimits, taskStore).Concat(additionalTools ?? []).ToArray(); - builder.Services + IMcpServerBuilder mcpBuilder = builder.Services .AddMcpServer() .WithStdioServerTransport() .WithTools(tools); + ConfigureAgentTaskFilter(mcpBuilder); + configureBuilder?.Invoke(mcpBuilder); + mcpBuilder.WithTasks(taskStore, options => ConfigureTasks(options, taskCapableTools, requiredTaskTools)); using IHost host = builder.Build(); - await host.RunAsync(cancellationToken).ConfigureAwait(false); + await host.StartAsync(cancellationToken).ConfigureAwait(false); + hostObserver?.Ready(new McpReadyInfo("stdio", null, Environment.ProcessId, "2026-07-28", false, true)); + await host.WaitForShutdownAsync(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) + 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, McpTaskStoreSettings? taskStoreSettings = null, IMcpHostObserver? hostObserver = null, IReadOnlyList<McpServerTool>? additionalTools = null, IReadOnlySet<string>? taskCapableTools = null, IReadOnlySet<string>? requiredTaskTools = null, Action<IMcpServerBuilder>? configureBuilder = 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)); @@ -63,29 +73,76 @@ public static class McpHost builder.Services.AddSingleton(registry); builder.Services.AddSingleton(runtime); builder.Services.AddHostedService<QueuedTaskWorker>(); - McpServerTool[] tools = CreateTools(registry, locale, visionLimits); - builder.Services.AddMcpServer().WithHttpTransport().WithTools(tools); + var taskStore = new PersistentMcpTaskStore(taskStoreSettings ?? DefaultTaskStoreSettings()); + McpServerTool[] tools = CreateTools(registry, locale, visionLimits, taskStore).Concat(additionalTools ?? []).ToArray(); + IMcpServerBuilder mcpBuilder = builder.Services.AddMcpServer() + .WithHttpTransport(options => options.Stateless = true) + .WithTools(tools); + ConfigureAgentTaskFilter(mcpBuilder); + configureBuilder?.Invoke(mcpBuilder); + mcpBuilder.WithTasks(taskStore, options => ConfigureTasks(options, taskCapableTools, requiredTaskTools)); 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))) + var stopwatch = Stopwatch.StartNew(); + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + hostObserver?.HttpRequestStarted(new McpHttpRequestStartInfo(context.Connection.Id, context.TraceIdentifier, + context.Connection.RemoteIpAddress?.ToString(), context.Connection.RemotePort, + context.Request.Method, context.Request.Path.Value ?? string.Empty)); + try { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; + if (context.Request.Headers.TryGetValue("Origin", out Microsoft.Extensions.Primitives.StringValues origins) && + origins.Any(origin => !IsLoopbackOrigin(origin))) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + } + else await next(context).ConfigureAwait(false); } - if (context.Request.Path == "/mcp" && HttpMethods.IsGet(context.Request.Method)) + finally { - context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; - return; + stopwatch.Stop(); + hostObserver?.HttpRequest(new McpHttpRequestInfo(timestamp, context.Connection.Id, context.TraceIdentifier, + context.Connection.RemoteIpAddress?.ToString(), context.Connection.RemotePort, + context.Connection.LocalIpAddress?.ToString(), context.Connection.LocalPort, + context.Request.Method, context.Request.Path.Value ?? string.Empty, context.Response.StatusCode, + stopwatch.Elapsed.TotalMilliseconds, context.Request.ContentType, context.Request.ContentLength, + context.Request.Headers.UserAgent.ToString(), context.Request.Headers["MCP-Protocol-Version"].ToString(), + context.Request.Headers["MCP-Method"].ToString(), context.Request.Headers["MCP-Name"].ToString())); } - await next(context).ConfigureAwait(false); }); app.MapMcp("/mcp"); - try { await app.RunAsync(cancellationToken).ConfigureAwait(false); } + try + { + await app.StartAsync(cancellationToken).ConfigureAwait(false); + hostObserver?.Ready(new McpReadyInfo("http", $"http://{listenHost}:{port}/mcp", Environment.ProcessId, "2026-07-28", true, true)); + await app.WaitForShutdownAsync(cancellationToken).ConfigureAwait(false); + } finally { httpClient.Dispose(); } } + private static McpTaskStoreSettings DefaultTaskStoreSettings() => + new(Path.Combine(Path.GetTempPath(), "agent-mcp", "mcp-tasks"), TimeSpan.FromHours(24), 1000); + + private static void ConfigureTasks(McpTasksOptions options, IReadOnlySet<string>? taskCapableTools, IReadOnlySet<string>? requiredTaskTools) + { + IReadOnlySet<string> names = taskCapableTools ?? new HashSet<string>(["agent_run"], StringComparer.Ordinal); + options.ExecutionModeSelector = context => + context.MatchedPrimitive is not McpServerTool tool ? McpTaskExecutionMode.Synchronous : + requiredTaskTools?.Contains(tool.ProtocolTool.Name) == true ? McpTaskExecutionMode.Required : + names.Contains(tool.ProtocolTool.Name) ? McpTaskExecutionMode.Optional : McpTaskExecutionMode.Synchronous; + } + + private static void ConfigureAgentTaskFilter(IMcpServerBuilder builder) + { + builder.WithRequestFilters(filters => filters.AddCallToolFilter(next => async (context, cancellationToken) => + { + CallToolResult result = await next(context, cancellationToken).ConfigureAwait(false); + if (context.Params?.Name == "agent_run" && result.IsError == true) + throw new McpProtocolException("The Agent task failed.", McpErrorCode.InternalError); + return result; + })); + } + private static bool IsLoopbackOrigin(string? origin) { return Uri.TryCreate(origin, UriKind.Absolute, out Uri? uri) && @@ -94,7 +151,7 @@ public static class McpHost (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) + private static McpServerTool[] CreateTools(TaskRegistry registry, LocaleCatalog locale, VisionLimits visionLimits, PersistentMcpTaskStore taskStore) { var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { @@ -102,7 +159,7 @@ public static class McpHost TypeInfoResolver = new DefaultJsonTypeInfoResolver(), UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow }; - var target = new AgentTools(registry, locale, visionLimits); + var target = new AgentTools(registry, locale, visionLimits, taskStore); McpServerTool[] tools = typeof(AgentTools) .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .Where(static method => method.GetCustomAttribute<McpServerToolAttribute>() is not null) |
