mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Liniting to address a set of build warnings
This commit is contained in:
parent
ea8bc80fe6
commit
faf289d67f
15 changed files with 57 additions and 90 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');");
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Documentation.Mcp.Infrastructure;
|
||||
|
||||
public class TemporaryFileStream : FileStream
|
||||
internal class TemporaryFileStream : FileStream
|
||||
{
|
||||
public static TemporaryFileStream Create()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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." };
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue