diff --git a/CHANGELOG.md b/CHANGELOG.md index a208bf54..2eee4cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,9 +111,14 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g - `setup all`: fallback consent prompt now explains the ambiguity (declined vs error) and offers to grant the permissions programmatically via `az login`; per-audience permission lists and consent URLs show per-server MCP names (e.g. `mcp_MailTools`) instead of a generic `Agent 365 Tools` for every audience. ### Removed +- `a365 develop-mcp approve` and `a365 develop-mcp block` commands — MCP server approval and blocking are now managed through the Microsoft Admin Center rather than the CLI. +- `a365 develop-mcp package-mcp-server` command — MCP server package generation for Microsoft Admin Center submission is no longer produced by the CLI. The underlying `PackageMCPServerHelper` and `IAgent365ToolingService.GetServerInfoAsync`/`ApproveServerAsync`/`BlockServerAsync` service methods were removed with it. - `a365 config` command family (`config init`, `config display`, `config permissions`) — replaced by `a365 setup all --agent-name` and `a365 setup permissions custom`. ### Breaking Changes +- **`a365 develop-mcp approve` removed** — server approval is now performed by tenant admins via the Microsoft Admin Center. +- **`a365 develop-mcp block` removed** — server blocking is now performed by tenant admins via the Microsoft Admin Center. +- **`a365 develop-mcp package-mcp-server` removed** — MCP server packages for Microsoft Admin Center submission are no longer produced by the CLI; admins onboard and manage servers directly through the Microsoft Admin Center. - **`a365 config init` removed** — replace with `a365 setup all --agent-name `. This creates the agent blueprint, configures permissions, and registers the messaging endpoint in one step without requiring a pre-existing config file. - **`a365 config display` removed** — use `a365 query-entra blueprint-scopes` to inspect live blueprint permissions and consent state. - **`a365 config permissions` removed** — replace with `a365 setup permissions custom --resource-app-id --scopes `. diff --git a/docs/commands/README.md b/docs/commands/README.md index b538b9d2..29153973 100644 --- a/docs/commands/README.md +++ b/docs/commands/README.md @@ -27,9 +27,6 @@ There is reference documentation for each command. | [develop-mcp list-servers](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-list-servers) | List MCP servers in a specific Dataverse environment. | | [develop-mcp publish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-publish) | Publish an MCP server to a Dataverse environment. | | [develop-mcp unpublish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-unpublish) | Unpublish an MCP server from a Dataverse environment. | -| [develop-mcp approve](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-approve) | Approve an MCP server. | -| [develop-mcp block](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-block) | Block an MCP server. | -| [develop-mcp package-mcp-server](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-package-mcp-server) | Generate MCP server package for submission on Microsoft admin center. | | [publish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/publish) | Update manifest.json ID values and publish the package. Configure federated identity and app role assignments. | | [query-entra](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/query-entra) | Query Microsoft Entra ID for agent information including scopes, permissions, and consent status. | | [query-entra blueprint-scopes](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/query-entra#query-entra-blueprint-scopes) | List configured scopes and consent status for the agent blueprint. | diff --git a/src/DEVELOPER.md b/src/DEVELOPER.md index 8cce7aaa..f0a0ec3f 100644 --- a/src/DEVELOPER.md +++ b/src/DEVELOPER.md @@ -89,10 +89,6 @@ The CLI provides a `develop-mcp` command for managing Model Context Protocol (MC - `a365 develop-mcp publish -e -s ` — Publish an MCP server to a Dataverse environment - `a365 develop-mcp unpublish -e -s ` — Unpublish an MCP server from a Dataverse environment -**Server Approval (Global Operations):** -- `a365 develop-mcp approve -s ` — Approve an MCP server -- `a365 develop-mcp block -s ` — Block an MCP server - **Key Features:** - **Azure CLI Style Parameters:** Uses named options (`--environment-id/-e`, `--server-name/-s`) for better UX - **Dry Run Support:** All commands support `--dry-run` for safe testing @@ -126,9 +122,6 @@ a365 develop-mcp publish \ # Quick unpublish with short aliases a365 develop-mcp unpublish -e "Default-12345678-1234-1234-1234-123456789abc" -s "msdyn_MyMcpServer" -# Approve a server (global operation) -a365 develop-mcp approve --server-name "msdyn_MyMcpServer" - # Test commands safely with dry-run a365 develop-mcp publish -e "myenv" -s "myserver" --dry-run diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs index 1f05a65f..7d05e4d5 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Agents.A365.DevTools.Cli.Helpers; using Microsoft.Agents.A365.DevTools.Cli.Models; using Microsoft.Agents.A365.DevTools.Cli.Services; using Microsoft.Extensions.Logging; using System.CommandLine; -using static Microsoft.Agents.A365.DevTools.Cli.Helpers.PackageMCPServerHelper; namespace Microsoft.Agents.A365.DevTools.Cli.Commands; @@ -37,9 +35,6 @@ public static Command CreateCommand( developMcpCommand.AddCommand(CreateListServersSubcommand(logger, toolingService)); developMcpCommand.AddCommand(CreatePublishSubcommand(logger, toolingService, graphApiService)); developMcpCommand.AddCommand(CreateUnpublishSubcommand(logger, toolingService)); - developMcpCommand.AddCommand(CreateApproveSubcommand(logger, toolingService)); - developMcpCommand.AddCommand(CreateBlockSubcommand(logger, toolingService)); - developMcpCommand.AddCommand(CreatePackageMCPServerSubCommand(logger, toolingService)); developMcpCommand.AddCommand(CreateRegisterExternalMcpServerSubcommand(logger, toolingService, graphApiService)); return developMcpCommand; @@ -458,229 +453,6 @@ private static Command CreateUnpublishSubcommand( return command; } - /// - /// Creates the approve subcommand - /// - private static Command CreateApproveSubcommand(ILogger logger, IAgent365ToolingService toolingService) - { - var command = new Command("approve", "Approve an MCP server"); - - var serverNameOption = new Option( - ["--server-name", "-s"], - description: "MCP server name to approve" - ); - serverNameOption.IsRequired = false; // Allow null so we can prompt - command.AddOption(serverNameOption); - - var dryRunOption = new Option( - name: "--dry-run", - description: "Show what would be done without executing" - ); - command.AddOption(dryRunOption); - - var verboseOption = new Option( - ["--verbose", "-v"], - description: "Enable verbose logging" - ); - command.AddOption(verboseOption); - - command.SetHandler(async (serverName, dryRun, verbose) => - { - _ = verbose; - try - { - // Validate and prompt for missing required arguments with security checks - if (string.IsNullOrWhiteSpace(serverName)) - { - serverName = InputValidator.PromptAndValidateRequiredInput("Enter MCP server name to approve: ", "Server name", 100); - if (string.IsNullOrWhiteSpace(serverName)) - { - logger.LogError("Server name is required"); - return; - } - } - else - { - // Validate provided server name - serverName = InputValidator.ValidateInput(serverName, "Server name"); - if (serverName == null) - { - logger.LogError("Invalid server name format"); - return; - } - } - } - catch (ArgumentException ex) - { - logger.LogError("Input validation failed: {Message}", ex.Message); - return; - } - - logger.LogInformation("Starting approve operation for server {ServerName}...", serverName); - - if (dryRun) - { - logger.LogInformation("[DRY RUN] Would read config from a365.config.json"); - logger.LogInformation("[DRY RUN] Would approve MCP server {ServerName}", serverName); - await Task.CompletedTask; - return; - } - - // Call service - var success = await toolingService.ApproveServerAsync(serverName); - - if (!success) - { - logger.LogError("Failed to approve MCP server {ServerName}", serverName); - return; - } - - logger.LogInformation("Successfully approved MCP server {ServerName}", serverName); - - }, serverNameOption, dryRunOption, verboseOption); - - return command; - } - - /// - /// Creates the block subcommand - /// - private static Command CreateBlockSubcommand(ILogger logger, IAgent365ToolingService toolingService) - { - var command = new Command("block", "Block an MCP server"); - - var serverNameOption = new Option( - ["--server-name", "-s"], - description: "MCP server name to block" - ); - serverNameOption.IsRequired = false; // Allow null so we can prompt - command.AddOption(serverNameOption); - - var dryRunOption = new Option( - name: "--dry-run", - description: "Show what would be done without executing" - ); - command.AddOption(dryRunOption); - - var verboseOption = new Option( - ["--verbose", "-v"], - description: "Enable verbose logging" - ); - command.AddOption(verboseOption); - - command.SetHandler(async (serverName, dryRun, verbose) => - { - _ = verbose; - try - { - // Validate and prompt for missing required arguments with security checks - if (string.IsNullOrWhiteSpace(serverName)) - { - serverName = InputValidator.PromptAndValidateRequiredInput("Enter MCP server name to block: ", "Server name", 100); - if (string.IsNullOrWhiteSpace(serverName)) - { - logger.LogError("Server name is required"); - return; - } - } - else - { - // Validate provided server name - serverName = InputValidator.ValidateInput(serverName, "Server name"); - if (serverName == null) - { - logger.LogError("Invalid server name format"); - return; - } - } - } - catch (ArgumentException ex) - { - logger.LogError("Input validation failed: {Message}", ex.Message); - return; - } - - logger.LogInformation("Starting block operation for server {ServerName}...", serverName); - - if (dryRun) - { - logger.LogInformation("[DRY RUN] Would read config from a365.config.json"); - logger.LogInformation("[DRY RUN] Would block MCP server {ServerName}", serverName); - await Task.CompletedTask; - return; - } - - // Call service - var success = await toolingService.BlockServerAsync(serverName); - - if (!success) - { - logger.LogError("Failed to block MCP server {ServerName}", serverName); - return; - } - - logger.LogInformation("Successfully blocked MCP server {ServerName}", serverName); - - }, serverNameOption, dryRunOption, verboseOption); - - return command; - } - - /// - /// Creates the package generation subcommand - /// - private static Command CreatePackageMCPServerSubCommand(ILogger logger, IAgent365ToolingService toolingService) - { - var command = new Command("package-mcp-server", "Generate MCP server package for submission on Microsoft admin center"); - - var serverNameOption = new Option("--server-name", "MCP server name") { IsRequired = true }; - var developerNameOption = new Option("--developer-name", "Publisher/developer display name") { IsRequired = true }; - var iconUrlOption = new Option("--icon-url", "Public URL to a PNG icon for the MCP server") { IsRequired = true }; - var outputPathOption = new Option("--output-path", "Target directory for the generated ZIP package") { IsRequired = true }; - var dryRunOption = new Option(name: "--dry-run", description: "Show what would be done without executing"); - var verboseOption = new Option( - ["--verbose", "-v"], - description: "Enable verbose logging" - ); - - command.AddOption(serverNameOption); - command.AddOption(developerNameOption); - command.AddOption(iconUrlOption); - command.AddOption(outputPathOption); - command.AddOption(dryRunOption); - command.AddOption(verboseOption); - - command.SetHandler(async (serverName, developerName, iconUrl, outputPath, dryRun, verbose) => - { - _ = verbose; - if (dryRun) - { - logger.LogInformation("[DRY RUN] Would query MCP servers management endpoint to fetch details of the MCP server"); - logger.LogInformation("[DRY RUN] Fetch the icon from the provided url"); - logger.LogInformation("[DRY RUN] Build the package content and put it in the target directory"); - await Task.CompletedTask; - return; - } - - logger.LogInformation("Starting package creation..."); - - try - { - var serverInfo = await toolingService.GetServerInfoAsync(serverName); - var manifest = PackageMCPServerHelper.GenerateManifestJson(serverInfo, developerName, logger); - var zipFilePath = PackageMCPServerHelper.BuildPackage(manifest, serverInfo, iconUrl, outputPath); - logger.LogInformation("Package was created successfully at {zipFilePath}", zipFilePath); - } - catch (Exception ex) - { - logger.LogError(ex, "Package creation failed"); - } - - }, serverNameOption, developerNameOption, iconUrlOption, outputPathOption, dryRunOption, verboseOption); - - return command; - } - /// /// Creates the register-external-mcp-server subcommand /// diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs index 97cb87d5..1730f54a 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs @@ -215,54 +215,4 @@ public static string[] GetAllScopes() } } - // PackageMCPServer constants - public static class PackageMCPServer - { - public const string OutlinePngIconFileName = "outline.png"; - public const string ColorPngIconFileName = "color.png"; - public const string ManifestFileName = "manifest.json"; - public const string TemplateManifestJson = - @" - { - ""$schema"": ""https://developer.microsoft.com/en-us/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"", - ""manifestVersion"": ""devPreview"", - ""agentConnectors"": [ - { - ""id"": ""11111111-1111-1111-1111-111111111112"", - ""displayName"": ""DUMMY_DISPLAY_NAME"", - ""description"": ""DUMMY_DESCRIPTION"", - ""toolSource"": { - ""remoteMcpServer"": { - ""mcpServerUrl"": ""https://example.com/mcpServer"", - ""authorization"": { - ""type"": ""None"" - } - } - } - } - ], - ""version"": ""1.0.0"", - ""id"": ""11111111-1111-1111-1111-111111111112"", - ""developer"": { - ""name"": ""DUMMY_DEVELOPER"", - ""websiteUrl"": ""https://go.microsoft.com/fwlink/?linkid=2138949"", - ""privacyUrl"": ""https://go.microsoft.com/fwlink/?linkid=2138865"", - ""termsOfUseUrl"": ""https://go.microsoft.com/fwlink/?linkid=2138950"" - }, - ""name"": { - ""short"": ""DUMMY_SHORT_NAME"", - ""full"": ""DUMMY_FULL_NAME"" - }, - ""description"": { - ""short"": ""DUMMY_SHORT_DESCRIPTION"", - ""full"": ""DUMMY_FULL_DESCRIPTION"" - }, - ""icons"": { - ""outline"": ""outline.png"", - ""color"": ""color.png"" - }, - ""accentColor"": ""#E0F6FC"" - }"; - } - } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/PackageMCPServerHelper.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/PackageMCPServerHelper.cs deleted file mode 100644 index c9dd6a63..00000000 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/PackageMCPServerHelper.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO.Compression; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using Microsoft.Agents.A365.DevTools.Cli.Constants; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.A365.DevTools.Cli.Helpers -{ - public class PackageMCPServerHelper - { - /// - /// Generates a manifest JSON for an MCP server package. - /// - /// Server information to include in the manifest - /// Name of the developer/publisher - /// Logger - /// JSON string containing the manifest - public static string GenerateManifestJson(ServerInfo p, string developerName, ILogger logger) - { - JsonNode root; - try - { - root = JsonNode.Parse(McpConstants.PackageMCPServer.TemplateManifestJson) ?? new JsonObject(); - } - catch - { - root = new JsonObject(); - } - - var obj = root as JsonObject ?? new JsonObject(); - - var displayName = p.McpServerDisplayName ?? string.Empty; - var shortDisplayName = displayName.Length <= 30 ? displayName : displayName.Substring(0, 30); - var fullDisplayName = displayName.Length <= 100 ? displayName : displayName.Substring(0, 100); - if (displayName.Length > 30) - { - logger.LogWarning("Short name truncated to 30 characters. Original '{Original}' -> '{Short}'", - displayName, shortDisplayName); - } - if (displayName.Length > 100) - { - logger.LogWarning("Full name truncated to 100 characters. Original '{Original}' -> '{Full}'", - displayName, fullDisplayName); - } - - var description = p.McpServerDescription ?? string.Empty; - var shortDescription = description.Length <= 80 ? description : description.Substring(0, 80); - if (description.Length > 80) - { - logger.LogWarning("Short description truncated to 80 characters. Original '{Original}' -> '{Short}'", - description, shortDescription); - } - - // Replace values. - if (obj["agentConnectors"] is JsonArray connectors && connectors.Count > 0 && connectors[0] is JsonObject c0) - { - Set(c0, "id", p.McpServerId); - Set(c0, "displayName", shortDisplayName); - Set(c0, "description", description); - - if (c0["toolSource"]?["remoteMcpServer"] is JsonObject rs) - { - Set(rs, "mcpServerUrl", p.McpServerUrl); - } - } - Set(obj, "id", p.McpServerId); - var developerObj = obj["developer"] as JsonObject ?? (JsonObject)(obj["developer"] = new JsonObject()); - var nameObj = obj["name"] as JsonObject ?? (JsonObject)(obj["name"] = new JsonObject()); - var descriptionObj = obj["description"] as JsonObject ?? (JsonObject)(obj["description"] = new JsonObject()); - - Set(developerObj, "name", developerName); - Set(nameObj, "short", shortDisplayName); - Set(nameObj, "full", fullDisplayName); - Set(descriptionObj, "short", shortDescription); - Set(descriptionObj, "full", description); - - return obj.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); - - static void Set(JsonNode? parent, string prop, string? value) - { - if (parent is JsonObject o) - { - o[prop] = value ?? string.Empty; - } - } - } - - /// - /// The method to build the MCP package as a zip file. - /// - /// JSON content for the manifest.json file - /// Server information used for package naming - /// Public URL to download the icon from - /// Directory where the ZIP package will be created - /// Full path to the created ZIP file - public static string BuildPackage(string manifestJson, ServerInfo info, string iconUrl, string outputPath) - { - Directory.CreateDirectory(outputPath); - - // Derive package name from server id - var baseName = "Package_" + info.McpServerId; - - // Basic sanitization for file name. - var invalidChars = Path.GetInvalidFileNameChars(); - var sb = new System.Text.StringBuilder(baseName.Length); - foreach (var ch in baseName) - { - sb.Append(invalidChars.Contains(ch) ? '_' : ch); - } - var safeName = sb.ToString(); - var zipFilePath = Path.Combine(outputPath, $"{safeName}.zip"); - - // Download icon (both outline.png and color.png will use same bytes) - byte[] iconBytes; - using (var httpClient = new HttpClient()) - { - using var response = httpClient.GetAsync(iconUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); - if (!response.IsSuccessStatusCode) - { - throw new InvalidOperationException($"Failed to download icon from '{iconUrl}'. HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); - } - - iconBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); - if (iconBytes.Length == 0) - { - throw new InvalidOperationException($"Downloaded icon from '{iconUrl}' is empty."); - } - } - - using (var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) - { - using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: false)) - { - // manifest.json - WriteTextToArchive(zipArchive, McpConstants.PackageMCPServer.ManifestFileName, manifestJson); - - // Icons - WriteByteToArchive(zipArchive, McpConstants.PackageMCPServer.OutlinePngIconFileName, iconBytes); - WriteByteToArchive(zipArchive, McpConstants.PackageMCPServer.ColorPngIconFileName, iconBytes); - } - } - - return zipFilePath; - } - - private static void WriteTextToArchive(ZipArchive zipArchive, string fileName, string text) - { - var entry = zipArchive.CreateEntry(fileName); - using var entryStream = entry.Open(); - using var writer = new StreamWriter(entryStream); - writer.Write(text); - } - - private static void WriteByteToArchive(ZipArchive zipArchive, string fileName, byte[] binary) - { - var entry = zipArchive.CreateEntry(fileName); - using var entryStream = entry.Open(); - using var bw = new BinaryWriter(entryStream); - bw.Write(binary); - } - - public sealed class ServerInfo - { - public string McpServerId { get; init; } = string.Empty; - public string McpServerDisplayName { get; init; } = string.Empty; - public string McpServerDescription { get; init; } = string.Empty; - public string McpServerUrl { get; init; } = string.Empty; - } - } -} diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/README.md b/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/README.md index 40939cd8..d35c7f71 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/README.md +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Helpers/README.md @@ -15,7 +15,6 @@ This folder contains utility helper classes that provide common functionality us | **ManifestHelper** | `ManifestHelper.cs` | Teams app manifest parsing and modification | | **SecretProtectionHelper** | `SecretProtectionHelper.cs` | Mask secrets in logs and output | | **TenantDetectionHelper** | `TenantDetectionHelper.cs` | Detect tenant from Azure CLI or environment | -| **PackageMCPServerHelper** | `PackageMCPServerHelper.cs` | Package MCP servers for deployment | | **ProjectSettingsSyncHelper** | `ProjectSettingsSyncHelper.cs` | Sync settings between project files | --- diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs index f57bfa76..f7d37593 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs @@ -7,8 +7,6 @@ using Microsoft.Extensions.Logging; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Nodes; -using static Microsoft.Agents.A365.DevTools.Cli.Helpers.PackageMCPServerHelper; namespace Microsoft.Agents.A365.DevTools.Cli.Services; @@ -278,30 +276,6 @@ private string BuildUnpublishMcpServerUrl(string environment, string environment return $"{baseUrl}/agents/dataverse/environments/{environmentId}/mcpServers/{serverName}/unpublish"; } - /// - /// Builds URL for approving an MCP server - /// - /// Environment name - /// MCP server name - /// Full URL for approve endpoint - private string BuildApproveMcpServerUrl(string environment, string serverName) - { - var baseUrl = BuildAgent365ToolsBaseUrl(environment); - return $"{baseUrl}/agents/mcpServers/{serverName}/approve"; - } - - /// - /// Builds URL for blocking an MCP server - /// - /// Environment name - /// MCP server name - /// Full URL for block endpoint - private string BuildBlockMcpServerUrl(string environment, string serverName) - { - var baseUrl = BuildAgent365ToolsBaseUrl(environment); - return $"{baseUrl}/agents/mcpServers/{serverName}/block"; - } - /// /// Builds URL for adding a BYO MCP server /// @@ -343,17 +317,6 @@ private string BuildProvisionIdentityUrl(string environment, string serverName) return $"{baseUrl}/agents/mcpServers/{Uri.EscapeDataString(serverName)}/provisionIdentity"; } - /// - /// Builds URL for getting MCP server details - /// - /// Environment name - /// URL for get MCP server endpoint - private string BuildGetMCPServerUrl(string environment) - { - var baseUrl = BuildAgent365ToolsBaseUrl(environment); - return $"{baseUrl}/agents/servers/MCPManagement"; - } - /// public async Task ListEnvironmentsAsync(CancellationToken cancellationToken = default) { @@ -647,237 +610,6 @@ public async Task UnpublishServerAsync( } } - /// - public async Task ApproveServerAsync( - string serverName, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(serverName)) - throw new ArgumentException("Server name cannot be null or empty", nameof(serverName)); - - try - { - // Load configuration - // Use environment from constructor - - // Build URL using private helper method - var endpointUrl = BuildApproveMcpServerUrl(_environment, serverName); - - // Generate correlation ID at workflow entry point - var correlationId = Internal.HttpClientFactory.GenerateCorrelationId(); - - _logger.LogDebug("Approving MCP server {ServerName} (CorrelationId: {CorrelationId})", serverName, correlationId); - _logger.LogDebug("Environment: {Env}", _environment); - _logger.LogDebug("Endpoint URL: {Url}", endpointUrl); - - // Get authentication token - var audience = ConfigConstants.GetAgent365ToolsResourceAppId(_environment); - _logger.LogDebug("Acquiring access token for audience: {Audience}", audience); - - var loginHint = await AzCliHelper.ResolveLoginHintAsync(); - var authToken = await _authService.GetAccessTokenAsync(audience, userId: loginHint, ct: cancellationToken); - if (string.IsNullOrWhiteSpace(authToken)) - { - _logger.LogError("Failed to acquire authentication token"); - return false; - } - - // Create authenticated HTTP client - using var httpClient = Internal.HttpClientFactory.CreateAuthenticatedClient(authToken, correlationId: correlationId); - - // Log request details - LogRequest("POST", endpointUrl); - - // Make request with empty content - var content = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); - using var response = await httpClient.PostAsync(endpointUrl, content, cancellationToken); - - // Validate response using common helper - var (isSuccess, responseContent) = await ValidateResponseAsync(response, "approve MCP server", cancellationToken); - if (!isSuccess) - { - return false; - } - - _logger.LogDebug("Successfully approved MCP server"); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to approve MCP server {ServerName}", serverName); - return false; - } - } - - /// - public async Task BlockServerAsync( - string serverName, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(serverName)) - throw new ArgumentException("Server name cannot be null or empty", nameof(serverName)); - - try - { - // Load configuration - // Use environment from constructor - - // Build URL using private helper method - var endpointUrl = BuildBlockMcpServerUrl(_environment, serverName); - - // Generate correlation ID at workflow entry point - var correlationId = Internal.HttpClientFactory.GenerateCorrelationId(); - - _logger.LogDebug("Blocking MCP server {ServerName} (CorrelationId: {CorrelationId})", serverName, correlationId); - _logger.LogDebug("Environment: {Env}", _environment); - _logger.LogDebug("Endpoint URL: {Url}", endpointUrl); - - // Get authentication token - var audience = ConfigConstants.GetAgent365ToolsResourceAppId(_environment); - _logger.LogDebug("Acquiring access token for audience: {Audience}", audience); - - var loginHint = await AzCliHelper.ResolveLoginHintAsync(); - var authToken = await _authService.GetAccessTokenAsync(audience, userId: loginHint, ct: cancellationToken); - if (string.IsNullOrWhiteSpace(authToken)) - { - _logger.LogError("Failed to acquire authentication token"); - return false; - } - - // Create authenticated HTTP client - using var httpClient = Internal.HttpClientFactory.CreateAuthenticatedClient(authToken, correlationId: correlationId); - - // Log request details - LogRequest("POST", endpointUrl); - - // Make request with empty content - var content = new StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); - using var response = await httpClient.PostAsync(endpointUrl, content, cancellationToken); - - // Validate response using common helper - var (isSuccess, responseContent) = await ValidateResponseAsync(response, "block MCP server", cancellationToken); - if (!isSuccess) - { - return false; - } - - _logger.LogDebug("Successfully blocked MCP server"); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to block MCP server {ServerName}", serverName); - return false; - } - } - - /// - public async Task GetServerInfoAsync(string serverName, CancellationToken cancellationToken = default) - { - var endpointUrl = BuildGetMCPServerUrl(_environment); - - // Generate correlation ID at workflow entry point - var correlationId = Internal.HttpClientFactory.GenerateCorrelationId(); - - _logger.LogDebug("Calling get MCP server for {ServerName} (CorrelationId: {CorrelationId})", serverName, correlationId); - _logger.LogDebug("Environment: {Env}", _environment); - _logger.LogDebug("Endpoint URL: {Url}", endpointUrl); - - // Get authentication token - var audience = ConfigConstants.GetAgent365ToolsResourceAppId(_environment); - _logger.LogDebug("Acquiring access token for audience: {Audience}", audience); - - var loginHint = await AzCliHelper.ResolveLoginHintAsync(); - var authToken = await _authService.GetAccessTokenAsync(audience, userId: loginHint); - if (string.IsNullOrWhiteSpace(authToken)) - { - _logger.LogError("Failed to acquire authentication token"); - throw new InvalidOperationException("Failed to acquire authentication token"); - } - _logger.LogDebug("Successfully acquired access token"); - - // Create authenticated HTTP client - using var httpClient = Internal.HttpClientFactory.CreateAuthenticatedClient(authToken, correlationId: correlationId); - - var requestObject = new - { - @params = new - { - name = "GetMCPServer", - arguments = new - { - mcpServerName = serverName - } - }, - method = "tools/call", - id = "1", - jsonrpc = "2.0" - }; - - var json = JsonSerializer.Serialize(requestObject); - - // Log request details - LogRequest("POST", endpointUrl, json); - - using var request = new HttpRequestMessage(HttpMethod.Post, endpointUrl) - { - Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") - }; - - // Add Accept headers - request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream")); - - // Send the request - using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - _logger.LogError("Failed to fetch MCP server details. Status: {Status}", response.StatusCode); - var errorContent = await response.Content.ReadAsStringAsync(); - _logger.LogError("Error response: {Error}", errorContent); - throw new InvalidOperationException("Failed to fetch MCP server details"); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - - _logger.LogDebug("Successfully received response from MCP servers management endpoint"); - - // Join all response data: lines (handles single or multi-line data segments) - var dataJson = string.Concat( - responseContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) - .Where(l => l.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) - .Select(l => l.Substring(5).Trim())); - - // Parse outer JSON-RPC - var root = JsonNode.Parse(dataJson)!; - var content = root["result"]?["content"]?.AsArray() - ?? throw new InvalidOperationException("Missing result.content"); - - // Find the first text chunk that contains inner JSON (starts with '{') - var innerJson = content - .Select(n => n?["text"]?.GetValue()) - .FirstOrDefault(t => t is { } s && s.TrimStart().StartsWith("{")) - ?? throw new InvalidOperationException("Inner JSON not found in content[].text"); - - // Parse inner JSON and read server - var server = JsonNode.Parse(innerJson)!["server"] - ?? throw new InvalidOperationException("Missing 'server' object"); - - string Get(string name) => - server[name]?.GetValue() is { } v && !string.IsNullOrWhiteSpace(v) - ? v - : throw new InvalidOperationException($"Missing/empty server.{name}"); - - return new ServerInfo - { - McpServerId = Get("id"), - McpServerDisplayName = Get("displayName"), - McpServerDescription = Get("description"), - McpServerUrl = Get("url") - }; - } - /// public async Task LogRegisterUsageAsync( string serverName, diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs index 0e5df40e..cb9b4eaa 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.Agents.A365.DevTools.Cli.Models; -using static Microsoft.Agents.A365.DevTools.Cli.Helpers.PackageMCPServerHelper; namespace Microsoft.Agents.A365.DevTools.Cli.Services; @@ -59,36 +58,6 @@ Task UnpublishServerAsync( string serverName, CancellationToken cancellationToken = default); - /// - /// Approves an MCP server - /// - /// MCP server name to approve - /// Cancellation token - /// True if successful, false otherwise - Task ApproveServerAsync( - string serverName, - CancellationToken cancellationToken = default); - - /// - /// Blocks an MCP server - /// - /// MCP server name to block - /// Cancellation token - /// True if successful, false otherwise - Task BlockServerAsync( - string serverName, - CancellationToken cancellationToken = default); - - /// - /// Gets MCP server information - /// - /// MCP server name - /// Cancellation token - /// ServerInfo - public Task GetServerInfoAsync( - string serverName, - CancellationToken cancellationToken = default); - /// /// Logs telemetry for register-external-mcp-server usage before processing begins. /// diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandRegressionTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandRegressionTests.cs index 4b0308bd..b123b867 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandRegressionTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandRegressionTests.cs @@ -40,9 +40,7 @@ public async Task DryRunMode_NeverCallsActualServices() new[] { "list-environments", "--dry-run" }, new[] { "list-servers", "-e", "test-env", "--dry-run" }, new[] { "publish", "-e", "test-env", "-s", "test-server", "--dry-run" }, - new[] { "unpublish", "-e", "test-env", "-s", "test-server", "--dry-run" }, - new[] { "approve", "-s", "test-server", "--dry-run" }, - new[] { "block", "-s", "test-server", "--dry-run" } + new[] { "unpublish", "-e", "test-env", "-s", "test-server", "--dry-run" } }; foreach (var commandArgs in dryRunCommands) @@ -56,8 +54,6 @@ public async Task DryRunMode_NeverCallsActualServices() await _mockToolingService.DidNotReceive().ListServersAsync(Arg.Any()); await _mockToolingService.DidNotReceive().PublishServerAsync(Arg.Any(), Arg.Any(), Arg.Any()); await _mockToolingService.DidNotReceive().UnpublishServerAsync(Arg.Any(), Arg.Any()); - await _mockToolingService.DidNotReceive().ApproveServerAsync(Arg.Any()); - await _mockToolingService.DidNotReceive().BlockServerAsync(Arg.Any()); } [Theory] @@ -66,10 +62,6 @@ public async Task DryRunMode_NeverCallsActualServices() [InlineData("publish", "-e", "test-env", "-s", "test-server")] [InlineData("publish", "--environment-id", "test-env", "--server-name", "test-server")] [InlineData("unpublish", "-e", "test-env", "-s", "test-server")] - [InlineData("approve", "-s", "test-server")] - [InlineData("approve", "--server-name", "test-server")] - [InlineData("block", "-s", "test-server")] - [InlineData("block", "--server-name", "test-server")] public async Task AzureCliStyleParameters_AreAcceptedCorrectly(string command, params string[] args) { // This test ensures we maintain Azure CLI compatibility with named options @@ -80,8 +72,6 @@ public async Task AzureCliStyleParameters_AreAcceptedCorrectly(string command, p _mockToolingService.PublishServerAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new PublishMcpServerResponse { Status = "Success" }); _mockToolingService.UnpublishServerAsync(Arg.Any(), Arg.Any()).Returns(true); - _mockToolingService.ApproveServerAsync(Arg.Any()).Returns(true); - _mockToolingService.BlockServerAsync(Arg.Any()).Returns(true); var fullCommand = new List { command }; fullCommand.AddRange(args); @@ -358,35 +348,6 @@ public async Task ServiceIntegration_UnpublishCommand_PassesCorrectParameters() await _mockToolingService.Received(1).UnpublishServerAsync(testEnvId, testServerName); } - [Theory] - [InlineData("approve")] - [InlineData("block")] - public async Task NewCommands_ApproveAndBlock_WorkCorrectly(string commandName) - { - // Regression test: Ensures newly implemented approve/block commands function properly - - // Arrange - var testServerName = "msdyn_TestServer"; - - _mockToolingService.ApproveServerAsync(testServerName).Returns(true); - _mockToolingService.BlockServerAsync(testServerName).Returns(true); - - // Act - var result = await _command.InvokeAsync(new[] { commandName, "-s", testServerName }); - - // Assert - result.Should().Be(0); - - if (commandName == "approve") - { - await _mockToolingService.Received(1).ApproveServerAsync(testServerName); - } - else - { - await _mockToolingService.Received(1).BlockServerAsync(testServerName); - } - } - [Fact] public void CommandStructure_HasNoPositionalArguments() { diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandTests.cs index b42bc20f..2cb113d4 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/DevelopMcpCommandTests.cs @@ -41,7 +41,7 @@ public void CreateCommand_HasAllExpectedSubcommands() var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService); // Assert - command.Subcommands.Should().HaveCount(8); + command.Subcommands.Should().HaveCount(5); var subcommandNames = command.Subcommands.Select(sc => sc.Name).ToList(); subcommandNames.Should().Contain(new[] @@ -50,9 +50,6 @@ public void CreateCommand_HasAllExpectedSubcommands() "list-servers", "publish", "unpublish", - "approve", - "block", - "package-mcp-server", "register-external-mcp-server" }); } @@ -183,73 +180,6 @@ public void UnpublishSubcommand_HasCorrectOptionsWithAliases() serverOption!.Aliases.Should().Contain("-s"); } - [Fact] - public void PackageMcpServerSubcommand_HasCorrectOptions() - { - // Act - var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService); - var subcommand = command.Subcommands.First(sc => sc.Name == "package-mcp-server"); - - // Assert - subcommand.Description.Should().Be("Generate MCP server package for submission on Microsoft admin center"); - - var options = subcommand.Options.ToList(); - options.Should().HaveCount(6); // serverName, developerName, iconUrl, outputPath, dry-run, verbose - - var optionNames = options.Select(o => o.Name).ToList(); - optionNames.Should().Contain("server-name"); - optionNames.Should().Contain("developer-name"); - optionNames.Should().Contain("icon-url"); - optionNames.Should().Contain("output-path"); - optionNames.Should().Contain("dry-run"); - optionNames.Should().Contain("verbose"); - - options.First(o => o.Name == "server-name").IsRequired.Should().BeTrue(); - options.First(o => o.Name == "developer-name").IsRequired.Should().BeTrue(); - options.First(o => o.Name == "icon-url").IsRequired.Should().BeTrue(); - options.First(o => o.Name == "output-path").IsRequired.Should().BeTrue(); - } - - [Fact] - public void ApproveSubcommand_IsImplementedWithCorrectOptions() - { - // Act - var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService); - var subcommand = command.Subcommands.First(sc => sc.Name == "approve"); - - // Assert - subcommand.Description.Should().Be("Approve an MCP server"); - - var options = subcommand.Options.ToList(); - var optionNames = options.Select(o => o.Name).ToList(); - optionNames.Should().Contain("server-name"); - optionNames.Should().Contain("dry-run"); - - // Verify server-name has short alias - var serverOption = options.FirstOrDefault(o => o.Name == "server-name"); - serverOption!.Aliases.Should().Contain("-s"); - } - - [Fact] - public void BlockSubcommand_IsImplementedWithCorrectOptions() - { - // Act - var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService); - var subcommand = command.Subcommands.First(sc => sc.Name == "block"); - - // Assert - subcommand.Description.Should().Be("Block an MCP server"); - - var options = subcommand.Options.ToList(); - var optionNames = options.Select(o => o.Name).ToList(); - optionNames.Should().Contain("server-name"); - optionNames.Should().Contain("dry-run"); - - // Verify server-name has short alias - var serverOption = options.FirstOrDefault(o => o.Name == "server-name"); - serverOption!.Aliases.Should().Contain("-s"); - } - [Fact] public void AllSubcommands_SupportDryRunOption() { @@ -324,8 +254,6 @@ public void RegisterExternalMcpServerSubcommand_HasAllExpectedOptions() [InlineData("unpublish", "environment-id", "-e")] [InlineData("publish", "server-name", "-s")] [InlineData("unpublish", "server-name", "-s")] - [InlineData("approve", "server-name", "-s")] - [InlineData("block", "server-name", "-s")] [InlineData("register-external-mcp-server", "server-name", "-s")] [InlineData("register-external-mcp-server", "server-url", "-u")] [InlineData("register-external-mcp-server", "auth-type", "-a")]