Liniting to address a set of build warnings

This commit is contained in:
Damian Hickey 2025-10-21 11:56:47 +02:00
parent ea8bc80fe6
commit faf289d67f
15 changed files with 57 additions and 90 deletions

View file

@ -9,6 +9,7 @@
<Product>Duende Documentation MCP Server</Product>
<MinVerTagPrefix>dmcp-</MinVerTagPrefix>
<MinVerMinimumMajorMinor>1.0</MinVerMinimumMajorMinor>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
</Project>

View file

@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
namespace Documentation.Mcp.Database;
public class FTSBlogArticle
internal class FTSBlogArticle
{
[Key]
public required string Id { get; init; }

View file

@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
namespace Documentation.Mcp.Database;
public class FTSDocsArticle
internal class FTSDocsArticle
{
[Key]
public required string Id { get; init; }

View file

@ -5,12 +5,16 @@ using System.ComponentModel.DataAnnotations;
namespace Documentation.Mcp.Database;
public class FTSSampleProject
internal class FTSSampleProject
{
[Key]
public required string Id { get; init; }
public required string Product { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public List<string> Files { get; init; } = new(0);
}

View file

@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore;
namespace Documentation.Mcp.Database;
public class McpDb : DbContext
internal class McpDb(DbContextOptions<McpDb> options) : DbContext(options)
{
public McpDb(DbContextOptions<McpDb> options)
: base(options) { }
public DbSet<State> State => Set<State>();
public DbSet<FTSDocsArticle> FTSDocsArticle => Set<FTSDocsArticle>();
public DbSet<FTSBlogArticle> FTSBlogArticle => Set<FTSBlogArticle>();
public DbSet<FTSSampleProject> FTSSampleProject => Set<FTSSampleProject>();
public async Task SetLastUpdateStateAsync(string key, DateTimeOffset value)
@ -39,20 +39,17 @@ public class McpDb : DbContext
public async Task<DateTimeOffset> GetLastUpdateStateAsync(string key)
{
var stateEntity = await State.FirstOrDefaultAsync(it => it.Key == key);
if (stateEntity == null)
{
return DateTimeOffset.MinValue;
}
return JsonSerializer.Deserialize<DateTimeOffset>(stateEntity.Value);
return stateEntity == null
? DateTimeOffset.MinValue
: JsonSerializer.Deserialize<DateTimeOffset>(stateEntity.Value);
}
public string? EscapeFtsQueryString(string? query)
public static string? EscapeFtsQueryString(string? query)
=> !string.IsNullOrEmpty(query)
? string.Join(" ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"")}\""))
: query;
public string? EscapeFtsQueryString(string? query, string joinWith)
public static string? EscapeFtsQueryString(string? query, string joinWith)
=> !string.IsNullOrEmpty(query)
? string.Join($" {joinWith} ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"")}\""))
: query;

View file

@ -3,58 +3,14 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Duende.Documentation.Mcp.Server.Database.Migrations;
/// <inheritdoc />
public partial class Initial : Migration
internal partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// migrationBuilder.CreateTable(
// name: "FTSBlogArticle",
// columns: table => new
// {
// Id = table.Column<string>(type: "TEXT", nullable: false),
// Title = table.Column<string>(type: "TEXT", nullable: false),
// Content = table.Column<string>(type: "TEXT", nullable: false)
// },
// constraints: table =>
// {
// table.PrimaryKey("PK_FTSBlogArticle", x => x.Id);
// });
//
// migrationBuilder.CreateTable(
// name: "FTSDocsArticle",
// columns: table => new
// {
// Id = table.Column<string>(type: "TEXT", nullable: false),
// Product = table.Column<string>(type: "TEXT", nullable: false),
// Title = table.Column<string>(type: "TEXT", nullable: false),
// Content = table.Column<string>(type: "TEXT", nullable: false)
// },
// constraints: table =>
// {
// table.PrimaryKey("PK_FTSDocsArticle", x => x.Id);
// });
//
// migrationBuilder.CreateTable(
// name: "FTSSampleProject",
// columns: table => new
// {
// Id = table.Column<string>(type: "TEXT", nullable: false),
// Product = table.Column<string>(type: "TEXT", nullable: false),
// Title = table.Column<string>(type: "TEXT", nullable: false),
// Description = table.Column<string>(type: "TEXT", nullable: false),
// Files = table.Column<string>(type: "TEXT", nullable: false)
// },
// constraints: table =>
// {
// table.PrimaryKey("PK_FTSSampleProject", x => x.Id);
// });
migrationBuilder.Sql(@"CREATE VIRTUAL TABLE FTSBlogArticle USING fts5(Id, Title, Content, tokenize = 'porter unicode61');");
migrationBuilder.Sql(@"CREATE VIRTUAL TABLE FTSDocsArticle USING fts5(Id, Product, Title, Content, tokenize = 'porter unicode61');");
migrationBuilder.Sql(@"CREATE VIRTUAL TABLE FTSSampleProject USING fts5(Id, Product, Title, Description, Files, tokenize = 'porter unicode61');");

View file

@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
namespace Documentation.Mcp.Database;
public class State
internal class State
{
[Key]
public required string Id { get; init; }

View file

@ -3,7 +3,7 @@
namespace Documentation.Mcp.Infrastructure;
public class TemporaryFileStream : FileStream
internal class TemporaryFileStream : FileStream
{
public static TemporaryFileStream Create()
{

View file

@ -5,7 +5,6 @@ using Documentation.Mcp.Database;
using Documentation.Mcp.Sources.Blog;
using Documentation.Mcp.Sources.Docs;
using Documentation.Mcp.Sources.Samples;
using Duende.Documentation.Mcp.Server.Database;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

View file

@ -12,7 +12,7 @@ using SimpleFeedReader;
namespace Documentation.Mcp.Sources.Blog;
public class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticleIndexer> logger) : BackgroundService
internal class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticleIndexer> logger) : BackgroundService
{
private readonly TimeSpan _maxAge = TimeSpan.FromDays(2);
private static readonly DateTime ReferenceDate = new(2024, 10, 01);
@ -59,14 +59,20 @@ public class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticleIn
}
await db.SaveChangesAsync(stoppingToken);
logger.LogInformation("Saved {count} blog articles", db.FTSBlogArticle.Count());
logger.LogInformation("Saved {Count} blog articles", db.FTSBlogArticle.Count());
await db.SetLastUpdateStateAsync("blog", DateTimeOffset.UtcNow);
logger.LogInformation("Finished blog indexer");
}
private async Task RunIndexerForDocumentAsync(string title, string? description, Uri? url, McpDb db, HttpClient httpClient, CancellationToken stoppingToken)
private static async Task RunIndexerForDocumentAsync(
string title,
string? description,
Uri? url,
McpDb db,
HttpClient httpClient,
CancellationToken stoppingToken)
{
if (url == null)
{

View file

@ -10,7 +10,7 @@ using ModelContextProtocol.Server;
namespace Documentation.Mcp.Sources.Blog;
[McpServerToolType]
public class BlogSearchTool(McpDb db)
internal class BlogSearchTool(McpDb db)
{
[McpServerTool(Name = "search_duende_blog", Title = "Search Duende Blog")]
[Description("Semantically search within the Duende blog for the given query.")]
@ -18,7 +18,7 @@ public class BlogSearchTool(McpDb db)
[Description("The search query. Keep it concise and specific to increase the likelihood of a match.")] string query)
{
var results = await db.FTSBlogArticle
.FromSqlRaw("SELECT * FROM FTSBlogArticle WHERE Title MATCH {0} OR Content MATCH {0} ORDER BY rank", db.EscapeFtsQueryString(query))
.FromSqlRaw("SELECT * FROM FTSBlogArticle WHERE Title MATCH {0} OR Content MATCH {0} ORDER BY rank", McpDb.EscapeFtsQueryString(query))
.AsNoTracking()
.Take(6)
.ToListAsync();
@ -44,19 +44,15 @@ public class BlogSearchTool(McpDb db)
[McpServerTool(Name = "fetch_duende_blog", Title = "Fetch specific article from Duende blog")]
[Description("Fetch a specific article from the Duende blog.")]
public async Task<string> Fetch(
[Description("The document id.")] string id)
public async Task<string> Fetch([Description("The document id.")] string id)
{
var result = await db.FTSBlogArticle
.FromSqlRaw("SELECT * FROM FTSBlogArticle WHERE Id = {0} ORDER BY rank", id)
.AsNoTracking()
.FirstOrDefaultAsync();
if (result == null)
{
return $"No data found for document: \"{id}\".";
}
return $"# {result.Title}\n\n{result.Content}";
return result == null
? $"No data found for document: \"{id}\"."
: $"# {result.Title}\n\n{result.Content}";
}
}

View file

@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Documentation.Mcp.Sources.Docs;
public class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
internal class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
{
private readonly TimeSpan _maxAge = TimeSpan.FromDays(2);
@ -40,12 +40,13 @@ public class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIn
var lastUpdate = await db.GetLastUpdateStateAsync("docs");
if (lastUpdate > DateTimeOffset.UtcNow.Add(-_maxAge))
{
logger.LogInformation("Skipping docs indexer, last update was {lastUpdate}", lastUpdate);
logger.LogInformation("Skipping docs indexer, last update was {LastUpdate}", lastUpdate);
return;
}
// Explore llms.txt
var llmsTxt = await httpClient.GetStringAsync("https://docs.duendesoftware.com/llms.txt", stoppingToken);
var llmsUrl = new Uri("https://docs.duendesoftware.com/llms.txt");
var llmsTxt = await httpClient.GetStringAsync(llmsUrl, stoppingToken);
var llmsMd = Markdig.Markdown.Parse(llmsTxt);
await db.FTSDocsArticle.ExecuteDeleteAsync(stoppingToken);
@ -57,19 +58,25 @@ public class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIn
var title = link.Title ?? link.FirstChild?.ToString() ?? "Unknown";
var description = link.NextSibling is LiteralInline literal ? literal.Content.Text.TrimStart(':', ' ') : "";
await RunIndexerForDocumentAsync(title, description, link.Url!, db, httpClient, stoppingToken);
await RunIndexerForDocument(title, description, link.Url!, db, httpClient, stoppingToken);
}
}
await db.SaveChangesAsync(stoppingToken);
logger.LogInformation("Saved {count} docs articles", db.FTSDocsArticle.Count());
logger.LogInformation("Saved {Count} docs articles", db.FTSDocsArticle.Count());
await db.SetLastUpdateStateAsync("docs", DateTimeOffset.UtcNow);
logger.LogInformation("Finished docs indexer");
}
private async Task RunIndexerForDocumentAsync(string title, string description, string linkUrl, McpDb db, HttpClient httpClient, CancellationToken stoppingToken)
private static async Task RunIndexerForDocument(
string title,
string description,
string linkUrl,
McpDb db,
HttpClient httpClient,
CancellationToken stoppingToken)
{
// Start indexing
var llmsTxt = await httpClient.GetStringAsync(linkUrl, stoppingToken);

View file

@ -10,7 +10,7 @@ using ModelContextProtocol.Server;
namespace Documentation.Mcp.Sources.Docs;
[McpServerToolType]
public class DocsSearchTool(McpDb db)
internal class DocsSearchTool(McpDb db)
{
[McpServerTool(Name = "search_duende_docs", Title = "Search Duende Documentation")]
[Description("Semantically search within the Duende documentation for the given query.")]
@ -18,7 +18,7 @@ public class DocsSearchTool(McpDb db)
[Description("The search query. Keep it concise and specific to increase the likelihood of a match.")] string query)
{
var results = await db.FTSDocsArticle
.FromSqlRaw("SELECT * FROM FTSDocsArticle WHERE Title MATCH {0} OR Content MATCH {0} OR Product MATCH {0} ORDER BY rank", db.EscapeFtsQueryString(query))
.FromSqlRaw("SELECT * FROM FTSDocsArticle WHERE Title MATCH {0} OR Content MATCH {0} OR Product MATCH {0} ORDER BY rank", McpDb.EscapeFtsQueryString(query))
.AsNoTracking()
.Take(6)
.ToListAsync();

View file

@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging;
namespace Documentation.Mcp.Sources.Samples;
public class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
{
private readonly TimeSpan _maxAge = TimeSpan.FromDays(7);
@ -48,7 +48,8 @@ public class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleIndexe
}
// Explore llms.txt specific to samples
var llmsTxt = await httpClient.GetStringAsync("https://docs.duendesoftware.com/_llms-txt/identityserver-sample-code.txt", stoppingToken);
var llmsUri = new Uri("https://docs.duendesoftware.com/_llms-txt/identityserver-sample-code.txt");
var llmsTxt = await httpClient.GetStringAsync(llmsUri, stoppingToken);
llmsTxt = llmsTxt.Replace("###", "\n\n###"); // keep sample titles on separate lines (rehype in docs does minification)
llmsTxt = llmsTxt.Replace("* ", "\n* "); // keep lists on separate lines (rehype in docs does minification)
var llmsMd = Markdig.Markdown.Parse(llmsTxt);

View file

@ -10,7 +10,7 @@ using ModelContextProtocol.Server;
namespace Documentation.Mcp.Sources.Samples;
[McpServerToolType]
public class SamplesSearchTool(McpDb db)
internal class SamplesSearchTool(McpDb db)
{
[McpServerTool(Name = "search_duende_samples", Title = "Search Duende Code Samples")]
[Description("Search within the Duende code samples for the given query. Use this tool to find recent and relevant C# code samples.")]
@ -18,7 +18,7 @@ public class SamplesSearchTool(McpDb db)
[Description("The search query. Keep it concise and specific to increase the likelihood of a match.")] string query)
{
var results = await db.FTSSampleProject
.FromSqlRaw("SELECT * FROM FTSSampleProject WHERE Title MATCH {0} OR Description MATCH {0} OR Product MATCH {0} ORDER BY rank", db.EscapeFtsQueryString(query, "OR"))
.FromSqlRaw("SELECT * FROM FTSSampleProject WHERE Title MATCH {0} OR Description MATCH {0} OR Product MATCH {0} ORDER BY rank", McpDb.EscapeFtsQueryString(query, "OR"))
.AsNoTracking()
.Take(6)
.ToListAsync();
@ -88,7 +88,7 @@ public class SamplesSearchTool(McpDb db)
?? SampleProjectFile.NotFound();
}
public class SampleProject
internal class SampleProject
{
public static SampleProject NotFound() => new SampleProject { Title = "No data found.", Description = "" };
@ -97,7 +97,7 @@ public class SamplesSearchTool(McpDb db)
public List<SampleProjectFile> Files { get; set; } = new(0);
}
public class SampleProjectFile
internal class SampleProjectFile
{
public static SampleProjectFile NotFound() => new SampleProjectFile { Content = "No data found." };