File: Mcp\ExcludeFromMcpTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Mcp.Tools;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Otlp.Serialization;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Protocol;
 
namespace Aspire.Cli.Tests.Mcp;
 
public class ExcludeFromMcpTests
{
    private const string ApiServiceName = "api-service";
    private const string SecretServiceName = "secret-service";
 
    [Fact]
    public void IsExcludedFromMcp_ReturnsFalse_WhenPropertyNotSet()
    {
        var snapshot = new ResourceSnapshot
        {
            Name = "test",
            Properties = []
        };
 
        Assert.False(McpToolHelpers.IsExcludedFromMcp(snapshot));
    }
 
    [Fact]
    public void IsExcludedFromMcp_ReturnsTrue_WhenPropertyIsBoolTrue()
    {
        var snapshot = new ResourceSnapshot
        {
            Name = "test",
            Properties = new Dictionary<string, JsonNode?>
            {
                [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
            }
        };
 
        Assert.True(McpToolHelpers.IsExcludedFromMcp(snapshot));
    }
 
    [Fact]
    public void IsExcludedFromMcp_ReturnsFalse_WhenPropertyIsBoolFalse()
    {
        var snapshot = new ResourceSnapshot
        {
            Name = "test",
            Properties = new Dictionary<string, JsonNode?>
            {
                [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(false)
            }
        };
 
        Assert.False(McpToolHelpers.IsExcludedFromMcp(snapshot));
    }
 
    [Fact]
    public void IsExcludedFromMcp_ReturnsTrue_WhenPropertyIsStringTrue()
    {
        var snapshot = new ResourceSnapshot
        {
            Name = "test",
            Properties = new Dictionary<string, JsonNode?>
            {
                [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create("true")
            }
        };
 
        Assert.True(McpToolHelpers.IsExcludedFromMcp(snapshot));
    }
 
    [Fact]
    public void IsExcludedFromMcp_ReturnsFalse_WhenPropertyIsNull()
    {
        var snapshot = new ResourceSnapshot
        {
            Name = "test",
            Properties = new Dictionary<string, JsonNode?>
            {
                [KnownProperties.Resource.ExcludeFromMcp] = null
            }
        };
 
        Assert.False(McpToolHelpers.IsExcludedFromMcp(snapshot));
    }
 
    [Fact]
    public async Task ListResourcesTool_ExcludesResourceWithExcludeFromMcp()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = ApiServiceName,
                    DisplayName = "API Service",
                    ResourceType = "Project",
                    State = "Running"
                },
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running",
                    Properties = new Dictionary<string, JsonNode?>
                    {
                        [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
                    }
                }
            ],
            DashboardUrlsState = new DashboardUrlsState { BaseUrlWithLoginToken = "http://localhost:18888" }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var tool = new ListResourcesTool(monitor, NullLogger<ListResourcesTool>.Instance);
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(), CancellationToken.None).DefaultTimeout();
 
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains(ApiServiceName, textContent.Text);
        Assert.DoesNotContain(SecretServiceName, textContent.Text);
    }
 
    [Fact]
    public async Task ListResourcesTool_ReturnsNoResourcesFound_WhenAllExcluded()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running",
                    Properties = new Dictionary<string, JsonNode?>
                    {
                        [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
                    }
                }
            ],
            DashboardUrlsState = new DashboardUrlsState { BaseUrlWithLoginToken = "http://localhost:18888" }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var tool = new ListResourcesTool(monitor, NullLogger<ListResourcesTool>.Instance);
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(), CancellationToken.None).DefaultTimeout();
 
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains("No resources found", textContent.Text);
    }
 
    [Fact]
    public async Task ListConsoleLogsTool_ReturnsError_WhenResourceIsExcluded()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running",
                    Properties = new Dictionary<string, JsonNode?>
                    {
                        [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
                    }
                }
            ],
            LogLines = [new ResourceLogLine { Content = "secret log", IsError = false, ResourceName = SecretServiceName, LineNumber = 1 }]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var tool = new ListConsoleLogsTool(monitor, NullLogger<ListConsoleLogsTool>.Instance);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["resourceName"] = JsonDocument.Parse($"\"{SecretServiceName}\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Equal(McpToolHelpers.GetResourceNotAvailableMessage(SecretServiceName), textContent.Text);
    }
 
    [Fact]
    public async Task ListConsoleLogsTool_ReturnsLogs_WhenResourceIsNotExcluded()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = ApiServiceName,
                    DisplayName = "API Service",
                    ResourceType = "Project",
                    State = "Running"
                }
            ],
            LogLines = [new ResourceLogLine { Content = "Application started", IsError = false, ResourceName = ApiServiceName, LineNumber = 1 }]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var tool = new ListConsoleLogsTool(monitor, NullLogger<ListConsoleLogsTool>.Instance);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["resourceName"] = JsonDocument.Parse($"\"{ApiServiceName}\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError is null or false);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains("Application started", textContent.Text);
    }
 
    [Fact]
    public async Task ExecuteResourceCommandTool_ReturnsError_WhenResourceIsExcluded()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running",
                    Properties = new Dictionary<string, JsonNode?>
                    {
                        [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
                    }
                }
            ],
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var tool = new ExecuteResourceCommandTool(monitor, NullLogger<ExecuteResourceCommandTool>.Instance);
 
        using var doc = JsonDocument.Parse($$"""{"resourceName": "{{SecretServiceName}}", "commandName": "restart"}""");
        var arguments = doc.RootElement.EnumerateObject()
            .ToDictionary(p => p.Name, p => p.Value.Clone());
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Equal(McpToolHelpers.GetResourceNotAvailableMessage(SecretServiceName), textContent.Text);
    }
 
    [Fact]
    public async Task ListStructuredLogsTool_ReturnsError_WhenSpecificResourceIsExcluded()
    {
        var monitor = CreateMonitorWithDashboardAndExcludedResource();
        var (mockHttpClientFactory, _) = CreateMockHttpWithLogs(SecretServiceName);
 
        var tool = CreateStructuredLogsTool(monitor, mockHttpClientFactory);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["resourceName"] = JsonDocument.Parse($"\"{SecretServiceName}\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Equal(McpToolHelpers.GetResourceNotAvailableMessage(SecretServiceName), textContent.Text);
    }
 
    [Fact]
    public async Task ListStructuredLogsTool_FiltersExcludedResourceLogs_WhenNoResourceSpecified()
    {
        var monitor = CreateMonitorWithDashboardAndExcludedResource();
        var (mockHttpClientFactory, _) = CreateMockHttpWithLogs(ApiServiceName, SecretServiceName);
 
        var tool = CreateStructuredLogsTool(monitor, mockHttpClientFactory);
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError is null or false);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains(ApiServiceName, textContent.Text);
        Assert.DoesNotContain(SecretServiceName, textContent.Text);
    }
 
    [Fact]
    public async Task ListTracesTool_ReturnsError_WhenSpecificResourceIsExcluded()
    {
        var monitor = CreateMonitorWithDashboardAndExcludedResource();
        var (mockHttpClientFactory, _) = CreateMockHttpWithTraces(SecretServiceName);
 
        var tool = CreateTracesTool(monitor, mockHttpClientFactory);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["resourceName"] = JsonDocument.Parse($"\"{SecretServiceName}\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Equal(McpToolHelpers.GetResourceNotAvailableMessage(SecretServiceName), textContent.Text);
    }
 
    [Fact]
    public async Task ListTracesTool_FiltersExcludedResourceSpans_WhenNoResourceSpecified()
    {
        var monitor = CreateMonitorWithDashboardAndExcludedResource();
        var (mockHttpClientFactory, _) = CreateMockHttpWithTraces(ApiServiceName, SecretServiceName);
 
        var tool = CreateTracesTool(monitor, mockHttpClientFactory);
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError is null or false);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains(ApiServiceName, textContent.Text);
        Assert.DoesNotContain(SecretServiceName, textContent.Text);
    }
 
    [Fact]
    public async Task ListTraceStructuredLogsTool_FiltersExcludedResourceLogs()
    {
        var monitor = CreateMonitorWithDashboardAndExcludedResource();
        var (mockHttpClientFactory, _) = CreateMockHttpWithLogs(ApiServiceName, SecretServiceName);
 
        var tool = CreateTraceStructuredLogsTool(monitor, mockHttpClientFactory);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["traceId"] = JsonDocument.Parse("\"abc123def456\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError is null or false);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains(ApiServiceName, textContent.Text);
        Assert.DoesNotContain(SecretServiceName, textContent.Text);
    }
 
    [Fact]
    public async Task ListTraceStructuredLogsTool_ReturnsAllLogs_WhenNoResourcesExcluded()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            DashboardInfoResponse = new GetDashboardInfoResponse
            {
                ApiBaseUrl = "http://localhost:5000",
                ApiToken = "test-token",
                DashboardUrls = ["http://localhost:18888"]
            },
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = ApiServiceName,
                    DisplayName = "API Service",
                    ResourceType = "Project",
                    State = "Running"
                },
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running"
                }
            ]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var (mockHttpClientFactory, _) = CreateMockHttpWithLogs(ApiServiceName, SecretServiceName);
 
        var tool = CreateTraceStructuredLogsTool(monitor, mockHttpClientFactory);
        var arguments = new Dictionary<string, JsonElement>
        {
            ["traceId"] = JsonDocument.Parse("\"abc123def456\"").RootElement
        };
 
        var result = await tool.CallToolAsync(CallToolContextTestHelper.Create(arguments), CancellationToken.None).DefaultTimeout();
 
        Assert.True(result.IsError is null or false);
        var textContent = result.Content![0] as TextContentBlock;
        Assert.NotNull(textContent);
        Assert.Contains(ApiServiceName, textContent.Text);
        Assert.Contains(SecretServiceName, textContent.Text);
    }
 
    private static TestAuxiliaryBackchannelMonitor CreateMonitorWithDashboardAndExcludedResource()
    {
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            DashboardInfoResponse = new GetDashboardInfoResponse
            {
                ApiBaseUrl = "http://localhost:5000",
                ApiToken = "test-token",
                DashboardUrls = ["http://localhost:18888"]
            },
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = ApiServiceName,
                    DisplayName = "API Service",
                    ResourceType = "Project",
                    State = "Running"
                },
                new ResourceSnapshot
                {
                    Name = SecretServiceName,
                    DisplayName = "Secret Service",
                    ResourceType = "Project",
                    State = "Running",
                    Properties = new Dictionary<string, JsonNode?>
                    {
                        [KnownProperties.Resource.ExcludeFromMcp] = JsonValue.Create(true)
                    }
                }
            ]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
        return monitor;
    }
 
    private static (MockHttpClientFactory factory, MockHttpMessageHandler handler) CreateMockHttpWithLogs(params string[] serviceNames)
    {
        var resourceLogs = serviceNames.Select(name => new OtlpResourceLogsJson
        {
            Resource = new OtlpResourceJson
            {
                Attributes = [new OtlpKeyValueJson { Key = "service.name", Value = new OtlpAnyValueJson { StringValue = name } }]
            },
            ScopeLogs =
            [
                new OtlpScopeLogsJson
                {
                    Scope = new OtlpInstrumentationScopeJson { Name = "Microsoft.Extensions.Logging" },
                    LogRecords =
                    [
                        new OtlpLogRecordJson
                        {
                            TimeUnixNano = 1706540400000000000,
                            SeverityNumber = 9,
                            SeverityText = "Information",
                            Body = new OtlpAnyValueJson { StringValue = $"Log from {name}" },
                            Attributes = [new OtlpKeyValueJson { Key = OtlpHelpers.AspireLogIdAttribute, Value = new OtlpAnyValueJson { IntValue = 1 } }]
                        }
                    ]
                }
            ]
        }).ToArray();
 
        var resources = serviceNames.Select(name => new ResourceInfoJson
        {
            Name = name,
            InstanceId = null,
            HasLogs = true,
            HasTraces = true,
            HasMetrics = true
        }).ToArray();
 
        return CreateMockHttp(resources, new TelemetryApiResponse
        {
            Data = new OtlpTelemetryDataJson { ResourceLogs = resourceLogs },
            TotalCount = resourceLogs.Length,
            ReturnedCount = resourceLogs.Length
        });
    }
 
    private static (MockHttpClientFactory factory, MockHttpMessageHandler handler) CreateMockHttpWithTraces(params string[] serviceNames)
    {
        var resourceSpans = serviceNames.Select(name => new OtlpResourceSpansJson
        {
            Resource = new OtlpResourceJson
            {
                Attributes = [new OtlpKeyValueJson { Key = "service.name", Value = new OtlpAnyValueJson { StringValue = name } }]
            },
            ScopeSpans =
            [
                new OtlpScopeSpansJson
                {
                    Scope = new OtlpInstrumentationScopeJson { Name = "TestScope" },
                    Spans =
                    [
                        new OtlpSpanJson
                        {
                            Name = $"GET /{name}",
                            TraceId = "abc123def456",
                            SpanId = "span123",
                            StartTimeUnixNano = 1706540400000000000,
                            EndTimeUnixNano = 1706540400100000000,
                            Kind = 2 // Server
                        }
                    ]
                }
            ]
        }).ToArray();
 
        var resources = serviceNames.Select(name => new ResourceInfoJson
        {
            Name = name,
            InstanceId = null,
            HasLogs = true,
            HasTraces = true,
            HasMetrics = true
        }).ToArray();
 
        return CreateMockHttp(resources, new TelemetryApiResponse
        {
            Data = new OtlpTelemetryDataJson { ResourceSpans = resourceSpans },
            TotalCount = resourceSpans.Length,
            ReturnedCount = resourceSpans.Length
        });
    }
 
    private static (MockHttpClientFactory factory, MockHttpMessageHandler handler) CreateMockHttp(
        ResourceInfoJson[] resources,
        TelemetryApiResponse apiResponseObj)
    {
        var apiResponse = JsonSerializer.Serialize(apiResponseObj, OtlpJsonSerializerContext.Default.TelemetryApiResponse);
        var resourcesResponse = JsonSerializer.Serialize(resources, OtlpJsonSerializerContext.Default.ResourceInfoJsonArray);
 
        var handler = new MockHttpMessageHandler(request =>
        {
            if (request.RequestUri?.AbsolutePath.Contains("/resources") == true)
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(resourcesResponse, System.Text.Encoding.UTF8, "application/json")
                };
            }
 
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(apiResponse, System.Text.Encoding.UTF8, "application/json")
            };
        });
 
        return (new MockHttpClientFactory(handler), handler);
    }
 
    private static ListStructuredLogsTool CreateStructuredLogsTool(
        TestAuxiliaryBackchannelMonitor monitor,
        IHttpClientFactory httpClientFactory)
    {
        IDashboardInfoProvider dashboardInfoProvider = new BackchannelDashboardInfoProvider(monitor, NullLogger<BackchannelDashboardInfoProvider>.Instance);
        return new ListStructuredLogsTool(
            dashboardInfoProvider,
            monitor,
            httpClientFactory,
            NullLogger<ListStructuredLogsTool>.Instance);
    }
 
    private static ListTracesTool CreateTracesTool(
        TestAuxiliaryBackchannelMonitor monitor,
        IHttpClientFactory httpClientFactory)
    {
        IDashboardInfoProvider dashboardInfoProvider = new BackchannelDashboardInfoProvider(monitor, NullLogger<BackchannelDashboardInfoProvider>.Instance);
        return new ListTracesTool(
            dashboardInfoProvider,
            monitor,
            httpClientFactory,
            NullLogger<ListTracesTool>.Instance);
    }
 
    private static ListTraceStructuredLogsTool CreateTraceStructuredLogsTool(
        TestAuxiliaryBackchannelMonitor monitor,
        IHttpClientFactory httpClientFactory)
    {
        IDashboardInfoProvider dashboardInfoProvider = new BackchannelDashboardInfoProvider(monitor, NullLogger<BackchannelDashboardInfoProvider>.Instance);
        return new ListTraceStructuredLogsTool(
            dashboardInfoProvider,
            monitor,
            httpClientFactory,
            NullLogger<ListTraceStructuredLogsTool>.Instance);
    }
 
}