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.cs93
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)