Skip to content

Commit 6c8126c

Browse files
Add 15 MCP tools for book content, listings, guidelines, and search
- Add IGuidelinesService + GuidelinesService (singleton) for guidelines.json - Add BookListingTool: GetListingSourceCode, SearchListingsByCode - Add BookGuidelinesTool: GetCSharpGuidelines, GetGuidelinesByTopic - Add BookContentTool: GetSectionContent, GetListingWithContext, GetNavigationContext, GetChapterSummary - Extend BookSearchTool: GetChapterSections, GetDirectContentUrl, GetBookMetadata, LookupConcept, CheckTopicCoverage, FindBookHelpForDiagnostic, FindRelatedSections - Register all new tools in Program.cs MCP block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2e3ab51 commit 6c8126c

10 files changed

Lines changed: 1074 additions & 20 deletions

File tree

EssentialCSharp.Chat.Shared/Services/AIChatService.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -354,22 +354,17 @@ private static async Task<ResponseCreationOptions> CreateResponseOptionsAsync(
354354
Dictionary<string, object?> arguments = [];
355355
foreach (var kvp in jsonArguments)
356356
{
357-
if (kvp.Value is System.Text.Json.JsonElement jsonElement)
358-
{
359-
arguments[kvp.Key] = jsonElement.ValueKind switch
357+
arguments[kvp.Key] = kvp.Value is System.Text.Json.JsonElement jsonElement
358+
? jsonElement.ValueKind switch
360359
{
361360
System.Text.Json.JsonValueKind.String => jsonElement.GetString(),
362361
System.Text.Json.JsonValueKind.Number => jsonElement.GetDecimal(),
363362
System.Text.Json.JsonValueKind.True => true,
364363
System.Text.Json.JsonValueKind.False => false,
365364
System.Text.Json.JsonValueKind.Null => null,
366-
_ => jsonElement.ToString()
367-
};
368-
}
369-
else
370-
{
371-
arguments[kvp.Key] = kvp.Value;
372-
}
365+
_ => (object?)jsonElement.ToString()
366+
}
367+
: kvp.Value;
373368
}
374369

375370
var toolResult = await mcpClient.CallToolAsync(

EssentialCSharp.Web.Tests/McpTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async Task McpEndpoint_WithoutToken_Returns401()
3131
{
3232
HttpClient client = factory.CreateClient();
3333

34-
var request = CreateMcpInitializeRequest("/mcp");
34+
using var request = CreateMcpInitializeRequest("/mcp");
3535
using HttpResponseMessage response = await client.SendAsync(request);
3636

3737
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized);
@@ -65,7 +65,7 @@ public async Task McpEndpoint_WithValidToken_Returns200AndListsTools()
6565
HttpClient client = factory.CreateClient();
6666

6767
// Step 1: Initialize the MCP session
68-
var initRequest = CreateMcpInitializeRequest("/mcp");
68+
using var initRequest = CreateMcpInitializeRequest("/mcp");
6969
initRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", rawToken);
7070

7171
using HttpResponseMessage initResponse = await client.SendAsync(initRequest);
@@ -77,7 +77,7 @@ public async Task McpEndpoint_WithValidToken_Returns200AndListsTools()
7777
sessionId = sessionIdValues.First();
7878

7979
// Step 2: List tools
80-
var listToolsRequest = new HttpRequestMessage(HttpMethod.Post, "/mcp")
80+
using var listToolsRequest = new HttpRequestMessage(HttpMethod.Post, "/mcp")
8181
{
8282
Content = new StringContent(
8383
"""{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}""",
@@ -116,7 +116,7 @@ public async Task McpEndpoint_WithValidToken_Returns200AndListsTools()
116116
public async Task McpEndpoint_WithInvalidToken_Returns401()
117117
{
118118
HttpClient client = factory.CreateClient();
119-
var request = CreateMcpInitializeRequest("/mcp");
119+
using var request = CreateMcpInitializeRequest("/mcp");
120120
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "mcp_invalid_token_that_does_not_exist");
121121
using HttpResponseMessage response = await client.SendAsync(request);
122122
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized);
@@ -147,7 +147,7 @@ public async Task McpEndpoint_WithRevokedToken_Returns401()
147147
}
148148

149149
HttpClient client = factory.CreateClient();
150-
var request = CreateMcpInitializeRequest("/mcp");
150+
using var request = CreateMcpInitializeRequest("/mcp");
151151
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", rawToken);
152152
using HttpResponseMessage response = await client.SendAsync(request);
153153
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized);
@@ -179,7 +179,7 @@ public async Task McpEndpoint_WithExpiredToken_Returns401()
179179
}
180180

181181
HttpClient client = factory.CreateClient();
182-
var request = CreateMcpInitializeRequest("/mcp");
182+
using var request = CreateMcpInitializeRequest("/mcp");
183183
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", rawToken);
184184
using HttpResponseMessage response = await client.SendAsync(request);
185185
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized);

EssentialCSharp.Web/Controllers/McpTokenController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ public async Task<IActionResult> CreateToken(
2626
return BadRequest(new { Error = "Token name must be 256 characters or fewer." });
2727

2828
DateTime? expiresAt = null;
29-
if (request?.ExpiresOn.HasValue == true)
29+
if (request?.ExpiresOn is DateOnly expiresOn)
3030
{
31-
if (request.ExpiresOn.Value < DateOnly.FromDateTime(DateTime.UtcNow))
31+
if (expiresOn < DateOnly.FromDateTime(DateTime.UtcNow))
3232
return BadRequest(new { Error = "ExpiresOn must be today or in the future." });
3333
// Convert date-only boundary to end-of-day UTC instant before persisting
34-
expiresAt = request.ExpiresOn.Value.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
34+
expiresAt = expiresOn.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
3535
}
3636

3737
var (rawToken, entity) = await tokenService.CreateTokenAsync(

EssentialCSharp.Web/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,14 @@ private static void Main(string[] args)
260260
policy.AddAuthenticationSchemes("McpBearer")
261261
.RequireAuthenticatedUser()));
262262

263+
builder.Services.AddSingleton<IGuidelinesService, GuidelinesService>();
264+
263265
builder.Services.AddMcpServer()
264266
.WithHttpTransport(options => options.Stateless = true)
265-
.WithTools<BookSearchTool>();
267+
.WithTools<BookSearchTool>()
268+
.WithTools<BookListingTool>()
269+
.WithTools<BookGuidelinesTool>()
270+
.WithTools<BookContentTool>();
266271

267272
// Add Rate Limiting for API endpoints
268273
builder.Services.AddRateLimiter(options =>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using EssentialCSharp.Web.Extensions;
2+
3+
namespace EssentialCSharp.Web.Services;
4+
5+
public sealed class GuidelinesService : IGuidelinesService
6+
{
7+
private readonly IReadOnlyList<GuidelineListing> _guidelines;
8+
9+
public GuidelinesService(IWebHostEnvironment environment, ILogger<GuidelinesService> logger)
10+
{
11+
FileInfo fileInfo = new(Path.Join(environment.ContentRootPath, "Guidelines", "guidelines.json"));
12+
_guidelines = fileInfo.ReadGuidelineJsonFromInputDirectory(logger) ?? [];
13+
}
14+
15+
public IReadOnlyList<GuidelineListing> Guidelines => _guidelines;
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace EssentialCSharp.Web.Services;
2+
3+
public interface IGuidelinesService
4+
{
5+
IReadOnlyList<GuidelineListing> Guidelines { get; }
6+
}

0 commit comments

Comments
 (0)