File: Mcp\McpExtensions.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Utils;
using Aspire.Shared.Mcp;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
 
namespace Aspire.Dashboard.Mcp;
 
public static class McpExtensions
{
    public static IMcpServerBuilder AddAspireMcpTools(this IServiceCollection services, DashboardOptions dashboardOptions)
    {
        var builder = services.AddMcpServer(options =>
        {
            var icons = McpIconHelper.GetAspireIcons(typeof(McpExtensions).Assembly, "Aspire.Dashboard.Mcp.Resources");
 
            options.ServerInfo = new Implementation
            {
                Name = "Aspire MCP",
                Version = VersionHelpers.DashboardDisplayVersion ?? "1.0.0",
                Icons = icons
            };
            options.ServerInstructions =
                """
                ## Description
                This MCP Server provides various tools for managing Aspire resources, logs, traces and commands.
 
                ## Instructions
                - When a resource, structured log or trace is returned, include a link to the Aspire dashboard using dashboard_link
                - When a resource state (running, stopped, starting, ...) is returned, and add an emoji colored badge next to it (green, red, orange, etc).
 
                ## Tools
 
                """;
        }).WithHttpTransport();
 
        // Always register telemetry tools
        builder.WithTools<AspireTelemetryMcpTools>();
 
        // Only register resource tools if the resource service is configured
        if (dashboardOptions.ResourceServiceClient.GetUri() is not null)
        {
            builder.WithTools<AspireResourceMcpTools>();
        }
 
        builder
            .AddListToolsFilter((next) => async (RequestContext<ListToolsRequestParams> request, CancellationToken cancellationToken) =>
            {
                // Calls here are via the tools/list endpoint. See https://modelcontextprotocol.info/docs/concepts/tools/
                // There is no tool name so we hardcode name to list_tools here so we can reuse the same event.
                //
                // We want to track when users list tools as it's an indicator of whether Aspire MCP is configured (client tools refresh tools via it).
                // It's called even if no Aspire tools end up being used.
                return await RecordCallToolNameAsync<ListToolsRequestParams, ListToolsResult>(next, request, "list_tools", cancellationToken).ConfigureAwait(false);
            })
            .AddCallToolFilter((next) => async (RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken) =>
            {
                return await RecordCallToolNameAsync<CallToolRequestParams, CallToolResult>(next, request, request.Params?.Name, cancellationToken).ConfigureAwait(false);
            });
 
        return builder;
    }
 
    private static async Task<TResult> RecordCallToolNameAsync<TParams, TResult>(McpRequestHandler<TParams, TResult> next, RequestContext<TParams> request, string? toolCallName, CancellationToken cancellationToken)
    {
        // Record the tool name to telemetry.
        OperationContextProperty? operationId = null;
        var telemetryService = request.Services?.GetService<DashboardTelemetryService>();
        if (telemetryService != null && toolCallName != null)
        {
            var startToolCall = telemetryService.StartOperation(TelemetryEventKeys.McpToolCall,
                new Dictionary<string, AspireTelemetryProperty>
                {
                    { TelemetryPropertyKeys.McpToolName, new AspireTelemetryProperty(toolCallName) },
                });
 
            operationId = startToolCall.Properties.FirstOrDefault();
        }
 
        try
        {
            var result = await next(request, cancellationToken).ConfigureAwait(false);
 
            if (telemetryService is not null && operationId is not null)
            {
                telemetryService.EndOperation(operationId, TelemetryResult.Success);
            }
 
            return result;
        }
        catch (Exception ex)
        {
            if (telemetryService is not null && operationId is not null)
            {
                telemetryService.EndOperation(operationId, TelemetryResult.Failure, ex.Message);
            }
 
            throw;
        }
    }
}