// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Extensions.AI; using ModelContextProtocol.Client; namespace ExtAIBot; // Owns the McpClient lifetime, lists tools at startup, and returns them wrapped // with citation extraction so search results populate the CitationCollector. internal sealed class McpToolSet : IAsyncDisposable { private readonly McpClient _client; private readonly IList _tools; private readonly ILogger _logger; private McpToolSet(McpClient client, IList tools, ILogger logger) { _client = client; _tools = tools; _logger = logger; } public static async Task CreateAsync(ILogger logger, CancellationToken cancellationToken = default) { McpClient client = await McpClient.CreateAsync( new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri("https://learn.microsoft.com/api/mcp"), Name = "MSLearn", TransportMode = HttpTransportMode.StreamableHttp }), cancellationToken: cancellationToken); IList tools = await client.ListToolsAsync(cancellationToken: cancellationToken); return new McpToolSet(client, tools, logger); } // Returns each MCP tool wrapped so its results feed into the CitationCollector. public IList GetTools(CitationCollector citations) => [.. _tools.Select(t => new CitationCapturingTool(t, citations, _logger))]; public ValueTask DisposeAsync() => _client.DisposeAsync(); } internal sealed class McpToolSetLifetimeService(ILogger logger) : IHostedService { private McpToolSet? _value; public McpToolSet Value => _value ?? throw new InvalidOperationException("MCP tool set is not initialized."); public async Task StartAsync(CancellationToken cancellationToken) { _value = await McpToolSet.CreateAsync(logger, cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken) { if (_value is null) return; await _value.DisposeAsync(); _value = null; } } // Wraps an McpClientTool, delegating all metadata to it while intercepting // InvokeCoreAsync to extract citation data from the raw result string. file sealed class CitationCapturingTool(McpClientTool inner, CitationCollector citations, ILogger logger) : DelegatingAIFunction(inner) { protected override async ValueTask InvokeCoreAsync( AIFunctionArguments arguments, CancellationToken cancellationToken) { logger.LogInformation("[tool] {Name}({Args})", inner.Name, string.Join(", ", arguments.Select(a => $"{a.Key}={a.Value}"))); object? result = await inner.InvokeAsync(arguments, cancellationToken); if (result?.ToString() is string text) citations.TryExtract(text); return result; } }