|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Resources;
namespace Aspire.Cli.Utils.EnvironmentChecker;
/// <summary>
/// Checks for deprecated 'mcp start' command usage in agent environment configurations.
/// </summary>
internal sealed class DeprecatedAgentConfigCheck : IEnvironmentCheck
{
private readonly CliExecutionContext _executionContext;
// Define the agent config locations and their detection patterns
private static readonly AgentConfigLocation[] s_configLocations =
[
new AgentConfigLocation("Claude Code", ".mcp.json", ConfigFormat.McpServersWithArgs, "mcpServers"),
new AgentConfigLocation("VS Code", ".vscode/mcp.json", ConfigFormat.McpServersWithArgs, "servers"),
new AgentConfigLocation("Copilot CLI", ".github/copilot/mcp.json", ConfigFormat.McpServersWithArgs, "servers"),
new AgentConfigLocation("OpenCode", "opencode.jsonc", ConfigFormat.McpWithCommandArray, "mcp"),
];
public DeprecatedAgentConfigCheck(CliExecutionContext executionContext)
{
ArgumentNullException.ThrowIfNull(executionContext);
_executionContext = executionContext;
}
/// <inheritdoc />
public int Order => 100; // Run after core checks
/// <inheritdoc />
public Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
{
var results = new List<EnvironmentCheckResult>();
var workingDirectory = _executionContext.WorkingDirectory;
foreach (var location in s_configLocations)
{
var configPath = Path.Combine(workingDirectory.FullName, location.RelativePath);
if (!File.Exists(configPath))
{
continue;
}
try
{
var content = File.ReadAllText(configPath);
var config = JsonNode.Parse(content)?.AsObject();
if (config is null)
{
continue;
}
if (HasDeprecatedMcpCommand(config, location))
{
results.Add(new EnvironmentCheckResult
{
Category = "environment",
Name = $"agent-config-{location.AgentName.ToLowerInvariant().Replace(" ", "-")}",
Status = EnvironmentCheckStatus.Warning,
Message = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.DeprecatedConfigWarning, location.AgentName),
Fix = AgentCommandStrings.DeprecatedConfigFix
});
}
}
catch (JsonException)
{
// Skip malformed JSON files
}
catch (IOException)
{
// Skip files that can't be read
}
}
return Task.FromResult<IReadOnlyList<EnvironmentCheckResult>>(results);
}
/// <summary>
/// Checks for deprecated MCP command patterns in the config.
/// </summary>
private static bool HasDeprecatedMcpCommand(JsonObject config, AgentConfigLocation location)
{
return location.Format switch
{
ConfigFormat.McpServersWithArgs => HasDeprecatedMcpServersArgs(config, location.ServersKey),
ConfigFormat.McpWithCommandArray => HasDeprecatedMcpCommandArray(config, location.ServersKey),
_ => false
};
}
/// <summary>
/// Checks for deprecated pattern: {serversKey}.aspire.args = ["mcp", "start"]
/// Used by Claude Code (mcpServers), VS Code (servers), Copilot CLI (servers)
/// </summary>
private static bool HasDeprecatedMcpServersArgs(JsonObject config, string serversKey)
{
if (!config.TryGetPropertyValue(serversKey, out var serversNode) || serversNode is not JsonObject servers)
{
return false;
}
if (!servers.TryGetPropertyValue("aspire", out var aspireNode) || aspireNode is not JsonObject aspire)
{
return false;
}
if (!aspire.TryGetPropertyValue("args", out var argsNode) || argsNode is not JsonArray args)
{
return false;
}
// Check if args is ["mcp", "start"]
if (args.Count >= 2 &&
args[0]?.GetValue<string>() == "mcp" &&
args[1]?.GetValue<string>() == "start")
{
return true;
}
return false;
}
/// <summary>
/// Checks for deprecated pattern: {serversKey}.aspire.command = ["aspire", "mcp", "start"]
/// Used by OpenCode
/// </summary>
private static bool HasDeprecatedMcpCommandArray(JsonObject config, string serversKey)
{
if (!config.TryGetPropertyValue(serversKey, out var mcpNode) || mcpNode is not JsonObject mcp)
{
return false;
}
if (!mcp.TryGetPropertyValue("aspire", out var aspireNode) || aspireNode is not JsonObject aspire)
{
return false;
}
if (!aspire.TryGetPropertyValue("command", out var commandNode) || commandNode is not JsonArray command)
{
return false;
}
// Check if command is ["aspire", "mcp", "start"]
if (command.Count >= 3 &&
command[0]?.GetValue<string>() == "aspire" &&
command[1]?.GetValue<string>() == "mcp" &&
command[2]?.GetValue<string>() == "start")
{
return true;
}
return false;
}
/// <summary>
/// Represents a known agent configuration file location.
/// </summary>
private sealed record AgentConfigLocation(string AgentName, string RelativePath, ConfigFormat Format, string ServersKey);
/// <summary>
/// Configuration file format for detecting deprecated commands.
/// </summary>
private enum ConfigFormat
{
/// <summary>
/// Format: {serversKey}.aspire.args = ["mcp", "start"]
/// </summary>
McpServersWithArgs,
/// <summary>
/// Format: {serversKey}.aspire.command = ["aspire", "mcp", "start"]
/// </summary>
McpWithCommandArray
}
}
|