Skip to content

Commit 6c3b372

Browse files
Copilotjongalloway
andcommitted
Add MCP Resources support for .NET context
Co-authored-by: jongalloway <[email protected]>
1 parent 7bdc7dc commit 6c3b372

File tree

3 files changed

+244
-1
lines changed

3 files changed

+244
-1
lines changed

DotNetMcp/DotNetResources.cs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System.Diagnostics;
2+
using System.Text.Json;
3+
using Microsoft.Extensions.Logging;
4+
using ModelContextProtocol.Server;
5+
6+
namespace DotNetMcp;
7+
8+
/// <summary>
9+
/// MCP Resources for .NET environment information.
10+
/// Provides read-only access to .NET SDK, runtime, template, and framework metadata.
11+
/// </summary>
12+
[McpServerResourceType]
13+
public sealed class DotNetResources
14+
{
15+
private readonly ILogger<DotNetResources> _logger;
16+
17+
public DotNetResources(ILogger<DotNetResources> logger)
18+
{
19+
_logger = logger;
20+
}
21+
22+
[McpServerResource(
23+
UriTemplate = "dotnet://sdk-info",
24+
Name = ".NET SDK Information",
25+
Title = "Information about installed .NET SDKs including versions and paths",
26+
MimeType = "application/json")]
27+
public async Task<string> GetSdkInfo()
28+
{
29+
_logger.LogDebug("Reading SDK information");
30+
try
31+
{
32+
var result = await ExecuteDotNetCommandAsync("--list-sdks");
33+
34+
// Parse the SDK list output
35+
var sdks = new List<object>();
36+
var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries);
37+
38+
foreach (var line in lines)
39+
{
40+
// Format: "9.0.100 [C:\Program Files\dotnet\sdk]"
41+
var parts = line.Split('[', 2);
42+
if (parts.Length == 2)
43+
{
44+
var version = parts[0].Trim();
45+
var path = parts[1].TrimEnd(']').Trim();
46+
sdks.Add(new { version, path = System.IO.Path.Combine(path, version) });
47+
}
48+
}
49+
50+
var response = new
51+
{
52+
sdks,
53+
latestSdk = sdks.LastOrDefault() != null ? ((dynamic)sdks.Last()).version : null
54+
};
55+
56+
return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true });
57+
}
58+
catch (Exception ex)
59+
{
60+
_logger.LogError(ex, "Error getting SDK information");
61+
return JsonSerializer.Serialize(new { error = ex.Message });
62+
}
63+
}
64+
65+
[McpServerResource(
66+
UriTemplate = "dotnet://runtime-info",
67+
Name = ".NET Runtime Information",
68+
Title = "Information about installed .NET runtimes including versions and types",
69+
MimeType = "application/json")]
70+
public async Task<string> GetRuntimeInfo()
71+
{
72+
_logger.LogDebug("Reading runtime information");
73+
try
74+
{
75+
var result = await ExecuteDotNetCommandAsync("--list-runtimes");
76+
77+
// Parse the runtime list output
78+
var runtimes = new List<object>();
79+
var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries);
80+
81+
foreach (var line in lines)
82+
{
83+
// Format: "Microsoft.NETCore.App 9.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"
84+
var parts = line.Split('[', 2);
85+
if (parts.Length == 2)
86+
{
87+
var nameAndVersion = parts[0].Trim().Split(' ', 2);
88+
if (nameAndVersion.Length == 2)
89+
{
90+
var name = nameAndVersion[0];
91+
var version = nameAndVersion[1];
92+
var path = parts[1].TrimEnd(']').Trim();
93+
runtimes.Add(new { name, version, path = System.IO.Path.Combine(path, version) });
94+
}
95+
}
96+
}
97+
98+
var response = new { runtimes };
99+
return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true });
100+
}
101+
catch (Exception ex)
102+
{
103+
_logger.LogError(ex, "Error getting runtime information");
104+
return JsonSerializer.Serialize(new { error = ex.Message });
105+
}
106+
}
107+
108+
[McpServerResource(
109+
UriTemplate = "dotnet://templates",
110+
Name = "Template Catalog",
111+
Title = "Complete catalog of installed .NET templates with metadata",
112+
MimeType = "application/json")]
113+
public async Task<string> GetTemplates()
114+
{
115+
_logger.LogDebug("Reading template catalog");
116+
try
117+
{
118+
var templates = await TemplateEngineHelper.GetTemplatesCachedInternalAsync(_logger);
119+
120+
var templateList = templates.Select(t => new
121+
{
122+
name = t.Name,
123+
shortNames = t.ShortNameList.ToArray(),
124+
author = t.Author,
125+
language = t.GetLanguage(),
126+
type = t.GetTemplateType(),
127+
description = t.Description,
128+
parameters = t.ParameterDefinitions.Select(p => new
129+
{
130+
name = p.Name,
131+
description = p.Description,
132+
dataType = p.DataType,
133+
defaultValue = p.DefaultValue
134+
}).ToArray()
135+
}).ToArray();
136+
137+
return JsonSerializer.Serialize(new { templates = templateList }, new JsonSerializerOptions { WriteIndented = true });
138+
}
139+
catch (Exception ex)
140+
{
141+
_logger.LogError(ex, "Error getting template catalog");
142+
return JsonSerializer.Serialize(new { error = ex.Message });
143+
}
144+
}
145+
146+
[McpServerResource(
147+
UriTemplate = "dotnet://frameworks",
148+
Name = "Framework Information",
149+
Title = "Information about supported .NET frameworks (TFMs) including LTS status",
150+
MimeType = "application/json")]
151+
public Task<string> GetFrameworks()
152+
{
153+
_logger.LogDebug("Reading framework information");
154+
try
155+
{
156+
var modernFrameworks = FrameworkHelper.GetSupportedModernFrameworks()
157+
.Select(fw => new
158+
{
159+
tfm = fw,
160+
description = FrameworkHelper.GetFrameworkDescription(fw),
161+
isLts = FrameworkHelper.IsLtsFramework(fw),
162+
isModernNet = true,
163+
version = FrameworkHelper.GetFrameworkVersion(fw)
164+
}).ToArray();
165+
166+
var netCoreFrameworks = FrameworkHelper.GetSupportedNetCoreFrameworks()
167+
.Select(fw => new
168+
{
169+
tfm = fw,
170+
description = FrameworkHelper.GetFrameworkDescription(fw),
171+
isLts = FrameworkHelper.IsLtsFramework(fw),
172+
isNetCore = true,
173+
version = FrameworkHelper.GetFrameworkVersion(fw)
174+
}).ToArray();
175+
176+
var netStandardFrameworks = FrameworkHelper.GetSupportedNetStandardFrameworks()
177+
.Select(fw => new
178+
{
179+
tfm = fw,
180+
description = FrameworkHelper.GetFrameworkDescription(fw),
181+
isNetStandard = true,
182+
version = FrameworkHelper.GetFrameworkVersion(fw)
183+
}).ToArray();
184+
185+
var response = new
186+
{
187+
modernFrameworks,
188+
netCoreFrameworks,
189+
netStandardFrameworks,
190+
latestRecommended = FrameworkHelper.GetLatestRecommendedFramework(),
191+
latestLts = FrameworkHelper.GetLatestLtsFramework()
192+
};
193+
194+
return Task.FromResult(JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }));
195+
}
196+
catch (Exception ex)
197+
{
198+
_logger.LogError(ex, "Error getting framework information");
199+
return Task.FromResult(JsonSerializer.Serialize(new { error = ex.Message }));
200+
}
201+
}
202+
203+
/// <summary>
204+
/// Execute a dotnet command and return the output.
205+
/// </summary>
206+
private async Task<string> ExecuteDotNetCommandAsync(string args)
207+
{
208+
var startInfo = new ProcessStartInfo
209+
{
210+
FileName = "dotnet",
211+
Arguments = args,
212+
RedirectStandardOutput = true,
213+
RedirectStandardError = true,
214+
UseShellExecute = false,
215+
CreateNoWindow = true
216+
};
217+
218+
using var process = Process.Start(startInfo);
219+
if (process == null)
220+
{
221+
throw new InvalidOperationException("Failed to start dotnet process");
222+
}
223+
224+
var output = await process.StandardOutput.ReadToEndAsync();
225+
var error = await process.StandardError.ReadToEndAsync();
226+
await process.WaitForExitAsync();
227+
228+
if (process.ExitCode != 0 && !string.IsNullOrEmpty(error))
229+
{
230+
throw new InvalidOperationException($"dotnet command failed: {error}");
231+
}
232+
233+
return output;
234+
}
235+
}

DotNetMcp/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
builder.Services.AddMcpServer()
1414
.WithStdioServerTransport()
15-
.WithTools<DotNetCliTools>();
15+
.WithTools<DotNetCliTools>()
16+
.WithResources<DotNetResources>();
1617

1718
await builder.Build().RunAsync();

DotNetMcp/TemplateEngineHelper.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ private static async Task<IEnumerable<ITemplateInfo>> GetTemplatesCachedAsync(IL
6363
}
6464
}
6565

66+
/// <summary>
67+
/// Get templates from cache or load them if cache is expired (internal access for resources).
68+
/// This is intended for use by DotNetResources class to provide template data.
69+
/// </summary>
70+
internal static Task<IEnumerable<ITemplateInfo>> GetTemplatesCachedInternalAsync(ILogger? logger = null)
71+
=> GetTemplatesCachedAsync(logger);
72+
6673
/// <summary>
6774
/// Clear the template cache asynchronously. Useful after installing or uninstalling templates.
6875
/// </summary>

0 commit comments

Comments
 (0)