-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
167 lines (139 loc) · 7.25 KB
/
Copy pathProgram.cs
File metadata and controls
167 lines (139 loc) · 7.25 KB
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using System.IO.Abstractions;
using MarkdownRagMcp.Abstractions;
using MarkdownRagMcfg = MarkdownRagMcp.Configuration;
using MarkdownRagMcp.Chunking;
using MarkdownRagMcp.Embeddings;
using MarkdownRagMcp.Services;
using MarkdownRagMcp.Storage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.AspNetCore;
using ModelContextProtocol.Server;
// All shared services (file system, embedding pipeline, vector store, orchestration, preload)
// are wired once in AddMarkdownRagServices, so the stdio and HTTP hosts are configured
// identically. Only the transport and host type differ.
var transport = builder_GetTransport(args);
if (transport == "Http")
{
await RunHttpAsync(args);
}
else
{
await RunStdioAsync(args);
}
return;
// ---------------------------------------------------------------------------
// Stdio host: MCP speaks JSON-RPC over stdin/stdout, so all logging is
// redirected to stderr to avoid corrupting the protocol stream.
// ---------------------------------------------------------------------------
static async Task RunStdioAsync(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
AddMarkdownRagServices(builder.Services, builder.Configuration);
builder.Services
.AddMcpServer(ConfigureMcpServer)
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
}
// ---------------------------------------------------------------------------
// HTTP host: Streamable HTTP transport over Kestrel. stdout is not a protocol
// channel here, but logging is kept on stderr/stdout console for parity and
// so the same logs work under both hosts.
// ---------------------------------------------------------------------------
static async Task RunHttpAsync(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Keep logs off stdout corruption concerns and consistent with the stdio host.
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
AddMarkdownRagServices(builder.Services, builder.Configuration);
var transportSettings = builder.Configuration
.GetSection(MarkdownRagMcfg.MarkdownRagOptions.SectionName)
.Get<MarkdownRagMcfg.MarkdownRagOptions>()?.TransportSettings
?? new MarkdownRagMcfg.TransportOptions();
// When hosted under IIS/ANCM (in-process or out-of-process) the endpoint is supplied by
// the host and the address list is made read-only once the pipeline is built, so calling
// app.Run(url) throws "Changing the URL is not supported because Addresses IsReadOnly".
// Applying the configured URL on the host builder here instead: self-hosted runs still
// bind to the configured endpoint, while IIS runs use the ANCM-supplied address and the
// code never mutates the read-only list.
var hostedByAncm = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_IIS_PHYSICAL_PATH"));
if (!hostedByAncm && !string.IsNullOrWhiteSpace(transportSettings.HttpEndpointUrl))
{
builder.WebHost.UseUrls(transportSettings.HttpEndpointUrl);
}
builder.Services
.AddMcpServer(ConfigureMcpServer)
.WithHttpTransport(options =>
{
// Stateless is recommended for servers that never issue server-to-client
// requests (sampling/elicitation); this server only answers tool calls.
options.Stateless = transportSettings.Stateless;
})
.WithToolsFromAssembly();
var app = builder.Build();
// MapMcp exposes the Streamable HTTP endpoint; the optional prefix lets callers
// mount it under a subpath (e.g. "/mcp") instead of the root.
app.MapMcp(string.IsNullOrEmpty(transportSettings.HttpRoutePrefix) ? "" : transportSettings.HttpRoutePrefix);
app.Run();
}
// ---------------------------------------------------------------------------
// Shared DI wiring — identical for both transports.
// ---------------------------------------------------------------------------
static void AddMarkdownRagServices(IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<MarkdownRagMcfg.MarkdownRagOptions>()
.Bind(configuration.GetSection(MarkdownRagMcfg.MarkdownRagOptions.SectionName));
// File-system abstraction (testable; default is the real filesystem).
services.AddSingleton<IFileSystem, FileSystem>();
// Index tracking & file discovery.
services.AddSingleton<IIndexTracker, JsonIndexTracker>();
services.AddSingleton<IMarkdownFileDiscovery, MarkdownFileDiscovery>();
// Chunker + tokenizer (shared with the embedding provider so both use the model vocab).
services.AddSingleton<LazyBertTokenizer>();
services.AddSingleton<ITextTokenizer>(sp => sp.GetRequiredService<LazyBertTokenizer>());
services.AddSingleton<IMarkdownChunker, MarkdownChunker>();
// Embedding provider + first-run model downloader.
services.AddHttpClient<HuggingFaceModelDownloader>();
// Registered as both the interface and the concrete type: the preload hosted service needs
// the concrete type to call EnsureLoadedAsync (a non-interface method).
services.AddSingleton<OnnxEmbeddingProvider>();
services.AddSingleton<IEmbeddingProvider>(sp => sp.GetRequiredService<OnnxEmbeddingProvider>());
// Vector store (local-first in-memory, JSON-persisted).
services.AddSingleton<IVectorStore, InMemoryVectorStore>();
// Orchestration service consumed by the MCP tools.
services.AddSingleton<MarkdownRagService>();
// Preload the ONNX model in the background at startup so the first tool call doesn't pay
// the download + session-load cost inside the MCP request path (which can exceed a client's
// request timeout). Fire-and-forget; tool calls await the load if it's still in progress.
services.AddHostedService<EmbeddingPreloadHostedService>();
}
static void ConfigureMcpServer(McpServerOptions options)
{
options.ServerInfo = new() { Name = "mcp-markdown-rag", Version = "0.2.0" };
options.ServerInstructions = """
This MCP server provides semantic search capabilities over markdown files using
vector embeddings and a local vector database. It enables you to index markdown
documents and perform intelligent searches to find relevant content based on
semantic similarity rather than just keyword matching.
""";
}
// Read the Transport setting up front to decide which host to build. The configuration
// sources are the default ones (appsettings.json + environment overrides); a minimal
// configuration builder is enough because the real binding happens inside each host.
static string builder_GetTransport(string[] args)
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();
var section = config.GetSection(MarkdownRagMcfg.MarkdownRagOptions.SectionName);
var transport = section["Transport"];
return string.IsNullOrWhiteSpace(transport) ? "Stdio" : transport;
}