1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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);
}
}
}
}
|