-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCsharp.cs
More file actions
121 lines (109 loc) · 5.16 KB
/
Copy pathCsharp.cs
File metadata and controls
121 lines (109 loc) · 5.16 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
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
using Microsoft.Extensions.Logging;
namespace BlankSlate.Functions;
/// <summary>
/// Provides tools for retrieving best practices for the C# programming language.
/// Utilizes a process-wide cache to minimize disk reads and improve performance.
/// </summary>
/// <remarks>
/// This class is intended for internal use and is designed to be used within a function app context.
/// It reads best practices from a markdown file and caches the content for a short duration.
/// </remarks>
public class McpTools(ILogger<McpTools> logger)
{
// Simple process-wide cache to avoid disk reads on every invocation
private static readonly SemaphoreSlim CacheLock = new(1, 1);
private static string? _cachedContent;
private static DateTimeOffset _cachedFileWrite;
private static DateTimeOffset _cacheExpires;
[Function(nameof(GetCsharpBestPractices))]
public async Task<string> GetCsharpBestPractices(
[McpToolTrigger("get_csharp_best_practices", "Retrieves best practices for the C# programming language")]
ToolInvocationContext toolContext,
CancellationToken cancellationToken)
{
logger.ServingBestPractices("get_csharp_best_practices");
try
{
// Resolve path relative to the function app's working directory
var filePath = Path.Combine(AppContext.BaseDirectory, "Resources", "csharp-best-practices.md");
if (File.Exists(filePath))
{
var lastWrite = File.GetLastWriteTimeUtc(filePath);
// Return cached if still valid and file unchanged
if (_cachedContent is not null && _cacheExpires > DateTimeOffset.UtcNow && _cachedFileWrite == lastWrite)
{
logger.ServingCachedBestPractices(filePath);
return _cachedContent;
}
await CacheLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Double-check after acquiring the lock
if (_cachedContent is not null && _cacheExpires > DateTimeOffset.UtcNow && _cachedFileWrite == lastWrite)
{
logger.ServingCachedBestPractices(filePath);
return _cachedContent;
}
logger.LoadingBestPractices(filePath);
var content = await File.ReadAllTextAsync(filePath, cancellationToken).ConfigureAwait(false);
_cachedContent = content;
_cachedFileWrite = lastWrite;
_cacheExpires = DateTimeOffset.UtcNow.AddMinutes(5);
return content;
}
finally
{
CacheLock.Release();
}
}
else
{
logger.BestPracticesFileNotFound(filePath);
}
}
catch (OperationCanceledException)
{
// Propagate cancellation so the host can stop gracefully
throw;
}
catch (IOException ex)
{
logger.FailedToLoadBestPractices(ex);
}
catch (UnauthorizedAccessException ex)
{
logger.FailedToLoadBestPractices(ex);
}
catch (NotSupportedException ex)
{
logger.FailedToLoadBestPractices(ex);
}
string[] fallback = new[]
{
"# C# Best Practices",
"- Use meaningful names and clear intent.",
"- Keep methods small and single-purpose.",
"- Handle exceptions precisely; preserve stack traces.",
"- Embrace async/await and cancellation.",
"- Write tests and keep them deterministic.",
"- Follow SOLID and prefer immutability where practical."
};
return string.Join(Environment.NewLine, fallback);
}
}
// LoggerMessage source-generated helpers for better perf and analyzer compliance
internal static partial class McpToolsLogs
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Serving C# best practices via MCP tool {ToolName}")]
public static partial void ServingBestPractices(this ILogger logger, string toolName);
[LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "Loading best practices from {FilePath}")]
public static partial void LoadingBestPractices(this ILogger logger, string filePath);
[LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "Serving cached best practices content from {FilePath}")]
public static partial void ServingCachedBestPractices(this ILogger logger, string filePath);
[LoggerMessage(EventId = 4, Level = LogLevel.Warning, Message = "Best practices file not found at {FilePath}; serving fallback")]
public static partial void BestPracticesFileNotFound(this ILogger logger, string filePath);
[LoggerMessage(EventId = 5, Level = LogLevel.Error, Message = "Failed to load best practices content; serving fallback")]
public static partial void FailedToLoadBestPractices(this ILogger logger, Exception exception);
}