-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProgram.cs
More file actions
157 lines (146 loc) · 6.43 KB
/
Program.cs
File metadata and controls
157 lines (146 loc) · 6.43 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
using DotNetMcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Diagnostics;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
// Register ConcurrencyManager as a singleton
builder.Services.AddSingleton<ConcurrencyManager>();
// Register ProcessSessionManager as a singleton
builder.Services.AddSingleton<ProcessSessionManager>();
// Register InMemoryMcpTaskStore for async task support (MCP SDK v1.2.0 fixed #1430).
builder.Services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
// Register ToolMetricsAccumulator for in-memory telemetry collection.
// The accumulator is also captured by the telemetry filter added below.
var metricsAccumulator = new ToolMetricsAccumulator();
builder.Services.AddSingleton(metricsAccumulator);
// Register ResourceSubscriptionManager for tracking client resource subscriptions.
builder.Services.AddSingleton<ResourceSubscriptionManager>();
var mcpServerBuilder = builder.Services.AddMcpServer(options =>
{
// Configure server implementation with .NET-themed icon
options.ServerInfo = new Implementation
{
Name = "dotnet-mcp",
Version = typeof(Program).Assembly.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?.InformationalVersion
?? typeof(Program).Assembly.GetName().Version?.ToString()
?? "0.0.0",
Title = ".NET MCP Server",
Description = "MCP server providing AI assistants with direct access to the .NET SDK through both official NuGet packages and CLI execution",
WebsiteUrl = "https://github.com/jongalloway/dotnet-mcp",
Icons =
[
new Icon
{
Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Gear/Flat/gear_flat.svg",
MimeType = "image/svg+xml",
Sizes = ["any"],
Theme = "light"
},
new Icon
{
Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Gear/3D/gear_3d.png",
MimeType = "image/png",
Sizes = ["256x256"]
}
]
};
});
mcpServerBuilder
.WithStdioServerTransport()
.WithTools<DotNetCliTools>()
.WithResources<DotNetResources>()
.WithResources<McpAppsResources>()
.WithPrompts<DotNetPrompts>()
.WithSubscribeToResourcesHandler((context, ct) =>
{
var uri = context.Params?.Uri;
if (string.IsNullOrWhiteSpace(uri))
throw new McpException("Subscribe request must include a non-empty resource URI.");
context.Server.Services!.GetRequiredService<ResourceSubscriptionManager>().Subscribe(uri);
return ValueTask.FromResult(new EmptyResult());
})
.WithUnsubscribeFromResourcesHandler((context, ct) =>
{
var uri = context.Params?.Uri;
if (string.IsNullOrWhiteSpace(uri))
throw new McpException("Unsubscribe request must include a non-empty resource URI.");
context.Server.Services!.GetRequiredService<ResourceSubscriptionManager>().Unsubscribe(uri);
return ValueTask.FromResult(new EmptyResult());
})
.WithCompleteHandler(async (ctx, ct) =>
{
var argument = ctx.Params?.Argument;
if (argument is null)
return new CompleteResult { Completion = new Completion { Values = [] } };
var prefix = argument.Value ?? string.Empty;
var values = (await CompletionProvider.GetCompletionsAsync(argument.Name, prefix, ct)).ToList();
return new CompleteResult
{
Completion = new Completion { Values = values }
};
});
// Register the telemetry filter that intercepts every CallTool request to record
// invocation counts, durations, and success/failure rates — without modifying individual tools.
// The filter captures the shared metricsAccumulator instance via closure.
//
// The outermost filter also provides resilient error handling: if a tool throws an unhandled
// exception, the filter catches it and returns a structured CallToolResult with IsError = true
// instead of letting the exception propagate. This is critical for MCP Task support — without
// it, an unhandled exception causes the task to fail with "unknown error" because the SDK's
// task infrastructure has no CallToolResult to return to the client.
builder.Services.Configure<McpServerOptions>(options =>
{
options.Filters.Request.CallToolFilters.Add(next => async (context, ct) =>
{
var toolName = context.Params?.Name ?? "unknown";
var sw = Stopwatch.StartNew();
bool success = false;
try
{
var result = await next(context, ct);
success = true;
return result;
}
catch (OperationCanceledException)
{
// Let cancellations propagate normally — the SDK handles these for task cancellation.
throw;
}
catch (McpException)
{
// Let MCP protocol exceptions propagate — these carry proper JSON-RPC error codes.
throw;
}
catch (Exception ex)
{
// Convert unhandled exceptions to a structured error result so the MCP task
// lifecycle completes with a proper error instead of "unknown error".
// Redact the message and tool name to avoid leaking sensitive information
// (e.g., command-line arguments embedded in exception messages).
var redactedTool = SecretRedactor.Redact(toolName);
var redactedMessage = SecretRedactor.Redact(ex.Message);
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = $"Internal error in {redactedTool}: {redactedMessage}" }]
};
}
finally
{
sw.Stop();
metricsAccumulator.RecordInvocation(toolName, sw.ElapsedMilliseconds, success);
}
});
});
await builder.Build().RunAsync();