| File: Agents\Copilot\CopilotAgentEnvironmentScanner.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; using System.Text.Json.Nodes; using Aspire.Cli.Agents.Playwright; using Aspire.Cli.Resources; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Agents.Copilot; /// <summary> /// Scans for GitHub Copilot App and CLI environments and provides their shared configuration applicators. /// </summary> internal sealed class CopilotAgentEnvironmentScanner : IAgentEnvironmentScanner { private const string McpConfigFileName = "mcp-config.json"; private const string AspireServerName = "aspire"; private static readonly string s_skillBaseDirectory = Path.Combine(".github", "skills"); private readonly ICopilotCliRunner _copilotCliRunner; private readonly ICopilotAppInstallationDetector _copilotAppInstallationDetector; private readonly PlaywrightCliInstaller _playwrightCliInstaller; private readonly CliExecutionContext _executionContext; private readonly IEnvironment _environment; private readonly ILogger<CopilotAgentEnvironmentScanner> _logger; /// <summary> /// Initializes a new instance of <see cref="CopilotAgentEnvironmentScanner"/>. /// </summary> /// <param name="copilotCliRunner">The Copilot CLI runner for checking if Copilot CLI is installed.</param> /// <param name="copilotAppInstallationDetector">The detector for checking if the Copilot App is installed.</param> /// <param name="playwrightCliInstaller">The Playwright CLI installer for secure installation.</param> /// <param name="executionContext">The CLI execution context for accessing environment variables and settings.</param> /// <param name="environment">The environment abstraction for reading environment variables.</param> /// <param name="logger">The logger for diagnostic output.</param> public CopilotAgentEnvironmentScanner( ICopilotCliRunner copilotCliRunner, ICopilotAppInstallationDetector copilotAppInstallationDetector, PlaywrightCliInstaller playwrightCliInstaller, CliExecutionContext executionContext, IEnvironment environment, ILogger<CopilotAgentEnvironmentScanner> logger) { ArgumentNullException.ThrowIfNull(copilotCliRunner); ArgumentNullException.ThrowIfNull(copilotAppInstallationDetector); ArgumentNullException.ThrowIfNull(playwrightCliInstaller); ArgumentNullException.ThrowIfNull(executionContext); ArgumentNullException.ThrowIfNull(environment); ArgumentNullException.ThrowIfNull(logger); _copilotCliRunner = copilotCliRunner; _copilotAppInstallationDetector = copilotAppInstallationDetector; _playwrightCliInstaller = playwrightCliInstaller; _executionContext = executionContext; _environment = environment; _logger = logger; } /// <inheritdoc /> public async Task ScanAsync(AgentEnvironmentScanContext context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _logger.LogDebug("Starting GitHub Copilot environment scan"); var homeDirectory = _executionContext.HomeDirectory; var copilotAppDetected = false; if (_copilotAppInstallationDetector.GetInstallationMarker() is { } installationMarker) { _logger.LogDebug("Detected GitHub Copilot App using installation marker {Marker}", installationMarker); context.AddDetectedClient(AgentClientKind.CopilotApp); copilotAppDetected = true; } var copilotCliDetected = false; // Check if we're running in a VSCode terminal. var isVSCode = _environment.GetEnvironmentVariable("TERM_PROGRAM") == "vscode"; if (isVSCode) { _logger.LogDebug("Detected VSCode terminal environment. Assuming GitHub Copilot CLI is available to avoid potential hangs from interactive installation prompts."); context.AddDetectedClient(AgentClientKind.CopilotCli); copilotCliDetected = true; } else { _logger.LogDebug("Checking for GitHub Copilot CLI installation..."); var copilotVersion = await _copilotCliRunner.GetVersionAsync(cancellationToken).ConfigureAwait(false); if (copilotVersion is not null) { _logger.LogDebug("Found GitHub Copilot CLI version: {Version}", copilotVersion); context.AddDetectedClient(AgentClientKind.CopilotCli); copilotCliDetected = true; } else { _logger.LogDebug("GitHub Copilot CLI is not installed"); } } if (!copilotAppDetected && !copilotCliDetected) { _logger.LogDebug("No GitHub Copilot environments are installed - skipping"); return; } // The Copilot App automatically loads MCP servers and skills configured for Copilot CLI. // See https://docs.github.com/en/copilot/how-tos/github-copilot-app/customize-github-copilot-app. // Configure the shared Copilot locations once when either client is present. var configDirectory = CopilotPaths.GetConfigDirectory(homeDirectory, _environment); _logger.LogDebug("Checking if Aspire MCP server is already configured in GitHub Copilot"); if (!HasAspireServerConfigured(configDirectory)) { _logger.LogDebug("Adding GitHub Copilot applicator for global MCP configuration"); context.AddApplicator(CreateApplicator(configDirectory)); } else { _logger.LogDebug("Aspire MCP server is already configured in GitHub Copilot"); } CommonAgentApplicators.AddPlaywrightCliApplicator(context, _playwrightCliInstaller, s_skillBaseDirectory); } /// <summary> /// Gets the path to the GitHub Copilot MCP configuration file. /// </summary> /// <param name="configDirectory">The GitHub Copilot configuration directory.</param> private static string GetMcpConfigFilePath(string configDirectory) { return Path.Combine(configDirectory, McpConfigFileName); } /// <summary> /// Checks if the GitHub Copilot global configuration has an "aspire" MCP server configured. /// </summary> /// <param name="configDirectory">The GitHub Copilot configuration directory.</param> /// <returns>True if the aspire server is already configured, false otherwise.</returns> private static bool HasAspireServerConfigured(string configDirectory) => McpConfigFileHelper.HasServerConfigured( GetMcpConfigFilePath(configDirectory), "mcpServers", AspireServerName); /// <summary> /// Creates an applicator for configuring the MCP server in the GitHub Copilot global configuration. /// </summary> /// <param name="configDirectory">The GitHub Copilot configuration directory.</param> private static AgentEnvironmentApplicator CreateApplicator(string configDirectory) { return new AgentEnvironmentApplicator( CopilotAgentEnvironmentScannerStrings.ApplicatorDescription, ct => ApplyMcpConfigurationAsync( configDirectory, ct)); } /// <summary> /// Creates or updates the mcp-config.json file in the GitHub Copilot global configuration directory. /// </summary> /// <param name="configDirectory">The GitHub Copilot configuration directory.</param> /// <param name="cancellationToken">A cancellation token.</param> private static async Task ApplyMcpConfigurationAsync( string configDirectory, CancellationToken cancellationToken) { var configFilePath = GetMcpConfigFilePath(configDirectory); // Ensure the .copilot directory exists if (!Directory.Exists(configDirectory)) { Directory.CreateDirectory(configDirectory); } var config = await McpConfigFileHelper.ReadConfigAsync(configFilePath, null, cancellationToken); // Ensure "mcpServers" object exists if (!config.ContainsKey("mcpServers") || config["mcpServers"] is not JsonObject) { config["mcpServers"] = new JsonObject(); } var servers = config["mcpServers"]!.AsObject(); // Add or update the "aspire" server configuration with DOTNET_ROOT environment variable passthrough servers[AspireServerName] = new JsonObject { ["type"] = "local", ["command"] = "aspire", ["args"] = new JsonArray("agent", "mcp"), ["env"] = new JsonObject { ["DOTNET_ROOT"] = "${DOTNET_ROOT}" }, ["tools"] = new JsonArray("*") }; // Write the updated config using AOT-compatible serialization var jsonContent = JsonSerializer.Serialize(config, JsonSourceGenerationContext.Default.JsonObject); await File.WriteAllTextAsync(configFilePath, jsonContent, cancellationToken); } }