mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Address buid warnings and linting
Ignore some warnings around log performance (there is no performance issue)
This commit is contained in:
parent
faf289d67f
commit
44677af0f5
15 changed files with 94 additions and 86 deletions
|
|
@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace Documentation.Mcp.Database;
|
||||
|
||||
internal class FTSBlogArticle
|
||||
internal sealed class FTSBlogArticle
|
||||
{
|
||||
[Key]
|
||||
public required string Id { get; init; }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace Documentation.Mcp.Database;
|
||||
|
||||
internal class FTSDocsArticle
|
||||
internal sealed class FTSDocsArticle
|
||||
{
|
||||
[Key]
|
||||
public required string Id { get; init; }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace Documentation.Mcp.Database;
|
||||
|
||||
internal class FTSSampleProject
|
||||
internal sealed class FTSSampleProject
|
||||
{
|
||||
[Key]
|
||||
public required string Id { get; init; }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace Documentation.Mcp.Database;
|
||||
|
||||
internal class McpDb(DbContextOptions<McpDb> options) : DbContext(options)
|
||||
internal sealed class McpDb(DbContextOptions<McpDb> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<State> State => Set<State>();
|
||||
|
||||
|
|
@ -46,11 +46,11 @@ internal class McpDb(DbContextOptions<McpDb> options) : DbContext(options)
|
|||
|
||||
public static string? EscapeFtsQueryString(string? query)
|
||||
=> !string.IsNullOrEmpty(query)
|
||||
? string.Join(" ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"")}\""))
|
||||
? string.Join(" ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"", StringComparison.OrdinalIgnoreCase)}\""))
|
||||
: query;
|
||||
|
||||
public static string? EscapeFtsQueryString(string? query, string joinWith)
|
||||
=> !string.IsNullOrEmpty(query)
|
||||
? string.Join($" {joinWith} ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"")}\""))
|
||||
? string.Join($" {joinWith} ", query.Split(' ').Select(q => $"\"{q.Replace("\"", "\"\"", StringComparison.OrdinalIgnoreCase)}\""))
|
||||
: query;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||
namespace Duende.Documentation.Mcp.Server.Database.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal partial class Initial : Migration
|
||||
internal sealed partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace Documentation.Mcp.Database;
|
||||
|
||||
internal class State
|
||||
internal sealed class State
|
||||
{
|
||||
[Key]
|
||||
public required string Id { get; init; }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@
|
|||
<Title>Duende Documentation MCP Server</Title>
|
||||
<Copyright>Duende Software</Copyright>
|
||||
<PackageTags>MCP;ModelContextProtocol;AI;LLM;Duende;IdentityServer</PackageTags>
|
||||
<!--Use the LoggerMessage delegates - not a high performence scenario-->
|
||||
<NoWarn>$(NoWarn);CA1848</NoWarn>
|
||||
<!-- Evaluation of this argument may be expensive and unnecessary if logging is disabled - not a high performance scenario-->
|
||||
<NoWarn>$(NoWarn);CA1873</NoWarn>
|
||||
<!-- Avoid uninstantiated internal classes (code analysis) - https://github.com/dotnet/roslyn-analyzers/issues/6561 -->
|
||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@
|
|||
|
||||
namespace Documentation.Mcp.Infrastructure;
|
||||
|
||||
internal class TemporaryFileStream : FileStream
|
||||
internal sealed class TemporaryFileStream : FileStream
|
||||
{
|
||||
public static TemporaryFileStream Create()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid() + ".tmp");
|
||||
private readonly string _path;
|
||||
|
||||
private TemporaryFileStream(string path)
|
||||
: base(path, FileMode.OpenOrCreate, FileAccess.ReadWrite) => _path = path;
|
||||
|
||||
return new TemporaryFileStream(path);
|
||||
}
|
||||
|
||||
public static async Task<TemporaryFileStream> CreateFromAsync(Stream otherStream)
|
||||
{
|
||||
|
|
@ -24,10 +21,15 @@ internal class TemporaryFileStream : FileStream
|
|||
return temporaryFileStream;
|
||||
}
|
||||
|
||||
private readonly string _path;
|
||||
private static TemporaryFileStream Create()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid() + ".tmp");
|
||||
|
||||
return new TemporaryFileStream(path);
|
||||
}
|
||||
|
||||
private TemporaryFileStream(string path)
|
||||
: base(path, FileMode.OpenOrCreate, FileAccess.ReadWrite) => _path = path;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
|
|
@ -37,9 +39,13 @@ internal class TemporaryFileStream : FileStream
|
|||
{
|
||||
File.Delete(_path);
|
||||
}
|
||||
catch
|
||||
catch (IOException)
|
||||
{
|
||||
// Best-effort...
|
||||
// Best-effort cleanup - file may be locked or inaccessible
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Best-effort cleanup - insufficient permissions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ app.MapMcp();
|
|||
|
||||
await EnsureDb(app.Services, app.Logger);
|
||||
|
||||
app.Run();
|
||||
await app.RunAsync();
|
||||
|
||||
async Task EnsureDb(IServiceProvider services, ILogger logger)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using SimpleFeedReader;
|
|||
|
||||
namespace Documentation.Mcp.Sources.Blog;
|
||||
|
||||
internal class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticleIndexer> logger) : BackgroundService
|
||||
internal sealed class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticleIndexer> logger) : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _maxAge = TimeSpan.FromDays(2);
|
||||
private static readonly DateTime ReferenceDate = new(2024, 10, 01);
|
||||
|
|
@ -41,7 +41,7 @@ internal class BlogArticleIndexer(IServiceProvider services, ILogger<BlogArticle
|
|||
var lastUpdate = await db.GetLastUpdateStateAsync("blog");
|
||||
if (lastUpdate > DateTimeOffset.UtcNow.Add(-_maxAge))
|
||||
{
|
||||
logger.LogInformation("Skipping blog indexer, last update was {lastUpdate}", lastUpdate);
|
||||
logger.LogInformation("Skipping blog indexer, last update was {LastUpdate}", lastUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Documentation.Mcp.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -10,7 +11,7 @@ using ModelContextProtocol.Server;
|
|||
namespace Documentation.Mcp.Sources.Blog;
|
||||
|
||||
[McpServerToolType]
|
||||
internal class BlogSearchTool(McpDb db)
|
||||
internal sealed class BlogSearchTool(McpDb db)
|
||||
{
|
||||
[McpServerTool(Name = "search_duende_blog", Title = "Search Duende Blog")]
|
||||
[Description("Semantically search within the Duende blog for the given query.")]
|
||||
|
|
@ -24,19 +25,19 @@ internal class BlogSearchTool(McpDb db)
|
|||
.ToListAsync();
|
||||
|
||||
var responseBuilder = new StringBuilder();
|
||||
responseBuilder.Append($"## Query\n\n{query}\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Query\n\n{query}\n\n");
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
responseBuilder.Append($"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
return responseBuilder.ToString();
|
||||
}
|
||||
|
||||
responseBuilder.Append($"## Response\n\nResults found for: \"{query}\". Listing a document id and document title:\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nResults found for: \"{query}\". Listing a document id and document title:\n\n");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
responseBuilder.Append($"- [{result.Id}]({result.Title})\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"- [{result.Id}]({result.Title})\n");
|
||||
}
|
||||
|
||||
return responseBuilder.ToString();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Documentation.Mcp.Sources.Docs;
|
||||
|
||||
internal class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
|
||||
internal sealed class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _maxAge = TimeSpan.FromDays(2);
|
||||
|
||||
|
|
@ -56,9 +56,7 @@ internal class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticle
|
|||
if (link.Url?.Contains("_llms-txt/", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
var title = link.Title ?? link.FirstChild?.ToString() ?? "Unknown";
|
||||
var description = link.NextSibling is LiteralInline literal ? literal.Content.Text.TrimStart(':', ' ') : "";
|
||||
|
||||
await RunIndexerForDocument(title, description, link.Url!, db, httpClient, stoppingToken);
|
||||
await RunIndexerForDocument(title, link.Url!, db, httpClient, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,14 +70,14 @@ internal class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticle
|
|||
|
||||
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);
|
||||
var linkUri = new Uri(linkUrl);
|
||||
var llmsTxt = await httpClient.GetStringAsync(linkUri, stoppingToken);
|
||||
var llmsMd = Markdig.Markdown.Parse(llmsTxt);
|
||||
|
||||
string? articleTitle = null;
|
||||
|
|
@ -108,10 +106,7 @@ internal class DocsArticleIndexer(IServiceProvider services, ILogger<DocsArticle
|
|||
}
|
||||
}
|
||||
|
||||
if (articleContent != null)
|
||||
{
|
||||
articleContent.AppendLine(llmsTxt.Substring(block.Span.Start, block.Span.Length));
|
||||
}
|
||||
articleContent?.AppendLine(llmsTxt.Substring(block.Span.Start, block.Span.Length));
|
||||
}
|
||||
|
||||
if (articleTitle != null && articleContent != null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Documentation.Mcp.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -10,7 +11,7 @@ using ModelContextProtocol.Server;
|
|||
namespace Documentation.Mcp.Sources.Docs;
|
||||
|
||||
[McpServerToolType]
|
||||
internal class DocsSearchTool(McpDb db)
|
||||
internal sealed class DocsSearchTool(McpDb db)
|
||||
{
|
||||
[McpServerTool(Name = "search_duende_docs", Title = "Search Duende Documentation")]
|
||||
[Description("Semantically search within the Duende documentation for the given query.")]
|
||||
|
|
@ -24,19 +25,19 @@ internal class DocsSearchTool(McpDb db)
|
|||
.ToListAsync();
|
||||
|
||||
var responseBuilder = new StringBuilder();
|
||||
responseBuilder.Append($"## Query\n\n{query}\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Query\n\n{query}\n\n");
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
responseBuilder.Append($"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
return responseBuilder.ToString();
|
||||
}
|
||||
|
||||
responseBuilder.Append($"## Response\n\nResults found for: \"{query}\". Listing a document id and document title, followed by related product:\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nResults found for: \"{query}\". Listing a document id and document title, followed by related product:\n\n");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
responseBuilder.Append($"- [{result.Id}]({result.Title}) ({result.Product})\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"- [{result.Id}]({result.Title}) ({result.Product})\n");
|
||||
}
|
||||
|
||||
return responseBuilder.ToString();
|
||||
|
|
@ -52,11 +53,8 @@ internal class DocsSearchTool(McpDb db)
|
|||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return $"No data found for document: \"{id}\".";
|
||||
}
|
||||
|
||||
return result.Content;
|
||||
return result == null
|
||||
? $"No data found for document: \"{id}\"."
|
||||
: result.Content;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Documentation.Mcp.Sources.Samples;
|
||||
|
||||
internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
|
||||
internal sealed class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleIndexer> logger) : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _maxAge = TimeSpan.FromDays(7);
|
||||
|
||||
|
|
@ -43,19 +43,20 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
var lastUpdate = await db.GetLastUpdateStateAsync("samples");
|
||||
if (lastUpdate > DateTimeOffset.UtcNow.Add(-_maxAge))
|
||||
{
|
||||
logger.LogInformation("Skipping samples indexer, last update was {lastUpdate}", lastUpdate);
|
||||
logger.LogInformation("Skipping samples indexer, last update was {LastUpdate}", lastUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Explore llms.txt specific to samples
|
||||
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)
|
||||
llmsTxt = llmsTxt.Replace("###", "\n\n###", StringComparison.OrdinalIgnoreCase); // keep sample titles on separate lines (rehype in docs does minification)
|
||||
llmsTxt = llmsTxt.Replace("* ", "\n* ", StringComparison.OrdinalIgnoreCase); // keep lists on separate lines (rehype in docs does minification)
|
||||
var llmsMd = Markdig.Markdown.Parse(llmsTxt);
|
||||
|
||||
// Download samples repository blob
|
||||
await using var samplesRepositoryBlobStream = await httpClient.GetStreamAsync("https://github.com/duendesoftware/samples/archive/refs/heads/main.zip", stoppingToken);
|
||||
var samplesBlobUri = new Uri("https://github.com/duendesoftware/samples/archive/refs/heads/main.zip");
|
||||
await using var samplesRepositoryBlobStream = await httpClient.GetStreamAsync(samplesBlobUri, stoppingToken);
|
||||
await using var samplesRepositoryTempStream = await TemporaryFileStream.CreateFromAsync(samplesRepositoryBlobStream);
|
||||
await using var samplesRepositoryZipStream = new ZipArchive(samplesRepositoryTempStream, ZipArchiveMode.Read, leaveOpen: false);
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
}
|
||||
else if (sampleContent != null)
|
||||
{
|
||||
var files = await GetFilesForRepositoryPathAsync(sampleContent.ToString(), samplesRepositoryZipStream);
|
||||
var files = await GetFilesForRepositoryPathAsync(sampleContent.ToString(), samplesRepositoryZipStream, stoppingToken);
|
||||
if (files.Count > 0)
|
||||
{
|
||||
db.FTSSampleProject.Add(new FTSSampleProject
|
||||
|
|
@ -108,7 +109,7 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
|
||||
if (sampleTitle != null && sampleContent != null)
|
||||
{
|
||||
var files = await GetFilesForRepositoryPathAsync(sampleContent.ToString(), samplesRepositoryZipStream);
|
||||
var files = await GetFilesForRepositoryPathAsync(sampleContent.ToString(), samplesRepositoryZipStream, stoppingToken);
|
||||
if (files.Count > 0)
|
||||
{
|
||||
db.FTSSampleProject.Add(new FTSSampleProject
|
||||
|
|
@ -123,27 +124,30 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
}
|
||||
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
logger.LogInformation("Saved {count} samples", db.FTSSampleProject.Count());
|
||||
logger.LogInformation("Saved {Count} samples", db.FTSSampleProject.Count());
|
||||
|
||||
await db.SetLastUpdateStateAsync("samples", DateTimeOffset.UtcNow);
|
||||
|
||||
logger.LogInformation("Finished samples indexer");
|
||||
}
|
||||
|
||||
private string ExtractTitle(string markdownText)
|
||||
private static string ExtractTitle(string markdownText)
|
||||
{
|
||||
// Remove:
|
||||
// [Section titled “.......”](#custom-profile-service)
|
||||
var indexOfSection = markdownText.IndexOf("[Section", StringComparison.OrdinalIgnoreCase);
|
||||
if (indexOfSection > 0)
|
||||
{
|
||||
markdownText = markdownText.Substring(0, indexOfSection - 1);
|
||||
markdownText = markdownText[..(indexOfSection - 1)];
|
||||
}
|
||||
|
||||
return markdownText;
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetFilesForRepositoryPathAsync(string markdownText, ZipArchive repositoryArchive)
|
||||
private static async Task<List<string>> GetFilesForRepositoryPathAsync(
|
||||
string markdownText,
|
||||
ZipArchive repositoryArchive,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var files = new List<string>();
|
||||
|
||||
|
|
@ -160,7 +164,7 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
continue;
|
||||
}
|
||||
|
||||
var sampleRootPath = "samples-main" + link.Url!.Substring(sampleRootIndex);
|
||||
var sampleRootPath = $"samples-main{link.Url![sampleRootIndex..]}";
|
||||
var sampleEntries = repositoryArchive.Entries
|
||||
.Where(e =>
|
||||
|
||||
|
|
@ -176,8 +180,8 @@ internal class SamplesIndexer(IServiceProvider services, ILogger<DocsArticleInde
|
|||
|
||||
foreach (var sampleEntry in sampleEntries)
|
||||
{
|
||||
using var sampleEntryStream = new StreamReader(sampleEntry.Open());
|
||||
var sampleContents = await sampleEntryStream.ReadToEndAsync();
|
||||
using var sampleEntryStream = new StreamReader(await sampleEntry.OpenAsync(cancellationToken));
|
||||
var sampleContents = await sampleEntryStream.ReadToEndAsync(cancellationToken);
|
||||
|
||||
files.Add("File: `" + sampleEntry.FullName + "`:\n```\n" + sampleContents + "\n```");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Documentation.Mcp.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -10,7 +11,7 @@ using ModelContextProtocol.Server;
|
|||
namespace Documentation.Mcp.Sources.Samples;
|
||||
|
||||
[McpServerToolType]
|
||||
internal class SamplesSearchTool(McpDb db)
|
||||
internal sealed 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.")]
|
||||
|
|
@ -24,19 +25,19 @@ internal class SamplesSearchTool(McpDb db)
|
|||
.ToListAsync();
|
||||
|
||||
var responseBuilder = new StringBuilder();
|
||||
responseBuilder.Append($"## Query\n\n{query}\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Query\n\n{query}\n\n");
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
responseBuilder.Append($"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nNo results found for: \"{query}\"\n\nIf you'd like to retry the search, try changing the query to increase the likelihood of a match.");
|
||||
return responseBuilder.ToString();
|
||||
}
|
||||
|
||||
responseBuilder.Append($"## Response\n\nResults found for: \"{query}\". Listing a document id and document title, followed by related product and a description of the sample:\n\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"## Response\n\nResults found for: \"{query}\". Listing a document id and document title, followed by related product and a description of the sample:\n\n");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
responseBuilder.Append($"- [{result.Id}]({result.Title}) ({result.Product}) - Description: {result.Description}\n");
|
||||
responseBuilder.Append(CultureInfo.InvariantCulture, $"- [{result.Id}]({result.Title}) ({result.Product}) - Description: {result.Description}\n");
|
||||
}
|
||||
|
||||
return responseBuilder.ToString();
|
||||
|
|
@ -52,17 +53,14 @@ internal class SamplesSearchTool(McpDb db)
|
|||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return SampleProject.NotFound();
|
||||
}
|
||||
|
||||
return new SampleProject
|
||||
{
|
||||
Title = result.Title,
|
||||
Description = result.Description,
|
||||
Files = result.Files.Select(it => new SampleProjectFile { Content = it }).ToList()
|
||||
};
|
||||
return result == null
|
||||
? SampleProject.NotFound()
|
||||
: new SampleProject
|
||||
{
|
||||
Title = result.Title,
|
||||
Description = result.Description,
|
||||
Files = [.. result.Files.Select(it => new SampleProjectFile { Content = it })]
|
||||
};
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "fetch_duende_sample_file", Title = "Fetch a file from a specific sample from Duende Code Samples", UseStructuredContent = true)]
|
||||
|
|
@ -71,7 +69,7 @@ internal class SamplesSearchTool(McpDb db)
|
|||
[Description("The document id.")] string id,
|
||||
[Description("The file name.")] string filename)
|
||||
{
|
||||
filename = filename.Replace("wwwroot", "~");
|
||||
filename = filename.Replace("wwwroot", "~", StringComparison.Ordinal);
|
||||
|
||||
var result = await db.FTSSampleProject
|
||||
.FromSqlRaw("SELECT * FROM FTSSampleProject WHERE Id = {0} ORDER BY rank", id)
|
||||
|
|
@ -88,18 +86,18 @@ internal class SamplesSearchTool(McpDb db)
|
|||
?? SampleProjectFile.NotFound();
|
||||
}
|
||||
|
||||
internal class SampleProject
|
||||
internal sealed class SampleProject
|
||||
{
|
||||
public static SampleProject NotFound() => new SampleProject { Title = "No data found.", Description = "" };
|
||||
public static SampleProject NotFound() => new() { Title = "No data found.", Description = "" };
|
||||
|
||||
public required string Title { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public List<SampleProjectFile> Files { get; set; } = new(0);
|
||||
public List<SampleProjectFile> Files { get; set; } = [];
|
||||
}
|
||||
|
||||
internal class SampleProjectFile
|
||||
internal sealed class SampleProjectFile
|
||||
{
|
||||
public static SampleProjectFile NotFound() => new SampleProjectFile { Content = "No data found." };
|
||||
public static SampleProjectFile NotFound() => new() { Content = "No data found." };
|
||||
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue