diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs
index 41ff6afe..d45982f2 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs
@@ -38,6 +38,7 @@ public static Command CreateCommand(
developMcpCommand.AddCommand(CreateApproveSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreateBlockSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreatePackageMCPServerSubCommand(logger, toolingService));
+ developMcpCommand.AddCommand(CreateCustomServerSubcommand(logger, toolingService));
return developMcpCommand;
}
@@ -800,6 +801,124 @@ private static Command CreatePackageMCPServerSubCommand(ILogger logger, IAgent36
return command;
}
+ ///
+ /// Creates the create-custom-server subcommand
+ ///
+ private static Command CreateCustomServerSubcommand(ILogger logger, IAgent365ToolingService toolingService)
+ {
+ var command = new Command("create-custom-server", "Create a custom MCP server via the MCPManagement server");
+
+ var nameOption = new Option("--name", "Unique logical name for the custom MCP server (no whitespace)") { IsRequired = true };
+ var baseServerIdOption = new Option("--base-server-id", "Base server ID to extend (e.g. mcp_MailServer)") { IsRequired = true };
+ var displayNameOption = new Option(["--display-name", "-d"], description: "User-friendly display name");
+ var descriptionOption = new Option(["--description"], description: "Description of the custom MCP server");
+ var instructionsOption = new Option(["--instructions"], description: "AI agent instructions for how to use this server");
+ var selectedBaseToolsOption = new Option("--selected-base-tools", description: "Comma-separated list of tool names to select from the base server") { AllowMultipleArgumentsPerToken = true };
+ var environmentIdOption = new Option(["--environment-id", "-e"], description: "Dataverse environment ID (null = tenant-level)");
+ var dryRunOption = new Option("--dry-run", description: "Show what would be done without executing");
+ var configOption = new Option(["-c", "--config"], getDefaultValue: () => "a365.config.json", description: "Configuration file path");
+
+ command.AddOption(nameOption);
+ command.AddOption(baseServerIdOption);
+ command.AddOption(displayNameOption);
+ command.AddOption(descriptionOption);
+ command.AddOption(instructionsOption);
+ command.AddOption(selectedBaseToolsOption);
+ command.AddOption(environmentIdOption);
+ command.AddOption(dryRunOption);
+ command.AddOption(configOption);
+
+ command.SetHandler(async (name, baseServerId, displayName, description, instructions, selectedBaseTools, environmentId, dryRun) =>
+ {
+ try
+ {
+ name = InputValidator.ValidateInput(name, "Name") ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ logger.LogError("Invalid name format");
+ return;
+ }
+
+ baseServerId = InputValidator.ValidateInput(baseServerId, "Base server ID") ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(baseServerId))
+ {
+ logger.LogError("Invalid base server ID format");
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(environmentId))
+ {
+ environmentId = InputValidator.ValidateInput(environmentId, "Environment ID");
+ if (environmentId == null)
+ {
+ logger.LogError("Invalid environment ID format");
+ return;
+ }
+ }
+ }
+ catch (ArgumentException ex)
+ {
+ logger.LogError("Input validation failed: {Message}", ex.Message);
+ return;
+ }
+
+ logger.LogInformation("Starting create-custom-server operation for '{Name}' extending '{BaseServerId}'...", name, baseServerId);
+
+ if (dryRun)
+ {
+ logger.LogInformation("[DRY RUN] Would create custom MCP server '{Name}' extending '{BaseServerId}'", name, baseServerId);
+ logger.LogInformation("[DRY RUN] Display Name: {DisplayName}", displayName ?? "[not set]");
+ logger.LogInformation("[DRY RUN] Environment ID: {EnvironmentId}", environmentId ?? "[tenant-level]");
+ if (selectedBaseTools?.Length > 0)
+ {
+ logger.LogInformation("[DRY RUN] Selected base tools: {Tools}", string.Join(", ", selectedBaseTools));
+ }
+ await Task.CompletedTask;
+ return;
+ }
+
+ var request = new CreateCustomMcpServerRequest
+ {
+ Name = name,
+ BaseServerId = baseServerId,
+ DisplayName = displayName,
+ Description = description,
+ Instructions = instructions,
+ SelectedBaseTools = selectedBaseTools?.Length > 0 ? string.Join(",", selectedBaseTools) : null,
+ EnvironmentId = string.IsNullOrWhiteSpace(environmentId) ? null : environmentId
+ };
+
+ var response = await toolingService.CreateCustomMcpServerAsync(request);
+
+ if (response == null)
+ {
+ logger.LogError("Failed to create custom MCP server '{Name}'", name);
+ return;
+ }
+
+ logger.LogInformation("Successfully created custom MCP server '{Name}'", response.Name);
+ logger.LogInformation(" ID: {Id}", response.Id);
+ if (!string.IsNullOrWhiteSpace(response.DisplayName))
+ {
+ logger.LogInformation(" Display Name: {DisplayName}", response.DisplayName);
+ }
+ if (!string.IsNullOrWhiteSpace(response.Scope))
+ {
+ logger.LogInformation(" Scope: {Scope}", response.Scope);
+ }
+ logger.LogInformation(" Total Tools: {TotalTools}", response.TotalTools);
+ logger.LogInformation(" Active: {IsActive}", response.IsActive);
+ if (response.Mos3UploadSuccess)
+ {
+ logger.LogInformation(" MOS3 Upload: Success (Title ID: {TitleId})", response.Mos3TitleId);
+ }
+
+ }, nameOption, baseServerIdOption, displayNameOption, descriptionOption, instructionsOption,
+ selectedBaseToolsOption, environmentIdOption, dryRunOption);
+
+ return command;
+ }
+
///
/// Validates and sanitizes user input following Azure CLI security patterns
///
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs
index 29168b35..212cdf98 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/McpConstants.cs
@@ -10,12 +10,13 @@ public static class McpConstants
{
// WorkIQ Tools App ID
- public const string WorkIQToolsProdAppId = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1";
+ //public const string WorkIQToolsProdAppId = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1";
+ public const string WorkIQToolsProdAppId = "05879165-0320-489e-b644-f72b33f3edf0";
///
/// Agent 365 Tools identifier URI (used for admin consent URL construction).
///
- public const string Agent365ToolsIdentifierUri = "https://agent365.svc.cloud.microsoft";
+ public const string Agent365ToolsIdentifierUri = "https://test.agent365.svc.cloud.microsoft";
///
/// Name of the tooling manifest file
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs
new file mode 100644
index 00000000..f47cd490
--- /dev/null
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.A365.DevTools.Cli.Models;
+
+///
+/// Request model for creating a custom MCP server via the MCPManagement server
+///
+public class CreateCustomMcpServerRequest
+{
+ ///
+ /// Unique logical name for the custom MCP server (no whitespace)
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ ///
+ /// User-friendly display name
+ ///
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+
+ ///
+ /// Description of the custom MCP server
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// AI agent instructions for how to use this server
+ ///
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; set; }
+
+ ///
+ /// Base server ID to extend (e.g. "mcp_MailServer")
+ ///
+ [JsonPropertyName("baseServerId")]
+ public required string BaseServerId { get; set; }
+
+ ///
+ /// Optional subset of tools to select from the base server.
+ /// Comma-separated string matching the MCPManagement server's expected format.
+ ///
+ [JsonPropertyName("selectedBaseTools")]
+ public string? SelectedBaseTools { get; set; }
+
+ ///
+ /// Optional additional tools from other sources (Graph, Connector, etc.)
+ ///
+ [JsonPropertyName("additionalTools")]
+ public AdditionalToolRequest[]? AdditionalTools { get; set; }
+
+ ///
+ /// null = tenant-level, value = environment-level
+ ///
+ [JsonPropertyName("environmentId")]
+ public string? EnvironmentId { get; set; }
+}
+
+///
+/// Represents an additional tool to include from a non-base-server source
+///
+public class AdditionalToolRequest
+{
+ ///
+ /// The type of backend tool (see BackendToolType enum values)
+ ///
+ [JsonPropertyName("backendToolType")]
+ public int BackendToolType { get; set; }
+
+ ///
+ /// Microsoft Graph operation ID (used when backendToolType = 2)
+ ///
+ [JsonPropertyName("graphOperationId")]
+ public string? GraphOperationId { get; set; }
+
+ ///
+ /// Power Platform connector ID (used when backendToolType = 1)
+ ///
+ [JsonPropertyName("connectorId")]
+ public string? ConnectorId { get; set; }
+
+ ///
+ /// Power Platform connector operation ID (used when backendToolType = 1)
+ ///
+ [JsonPropertyName("connectorOperationId")]
+ public string? ConnectorOperationId { get; set; }
+}
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerResponse.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerResponse.cs
new file mode 100644
index 00000000..0138314d
--- /dev/null
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerResponse.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.A365.DevTools.Cli.Models;
+
+///
+/// Response model from the CreateCustomMCPServer MCPManagement tool call
+///
+public class CreateCustomMcpServerResponse
+{
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; set; }
+
+ [JsonPropertyName("baseServer")]
+ public CustomMcpBaseServer? BaseServer { get; set; }
+
+ [JsonPropertyName("tools")]
+ public CustomMcpTool[]? Tools { get; set; }
+
+ [JsonPropertyName("totalTools")]
+ public int TotalTools { get; set; }
+
+ [JsonPropertyName("scope")]
+ public string? Scope { get; set; }
+
+ [JsonPropertyName("isActive")]
+ public bool IsActive { get; set; }
+
+ [JsonPropertyName("environmentId")]
+ public string? EnvironmentId { get; set; }
+
+ [JsonPropertyName("createdOn")]
+ public string? CreatedOn { get; set; }
+
+ [JsonPropertyName("modifiedOn")]
+ public string? ModifiedOn { get; set; }
+
+ [JsonPropertyName("packageBase64")]
+ public string? PackageBase64 { get; set; }
+
+ [JsonPropertyName("mos3TitleId")]
+ public string? Mos3TitleId { get; set; }
+
+ [JsonPropertyName("mos3UploadSuccess")]
+ public bool Mos3UploadSuccess { get; set; }
+}
+
+///
+/// Information about the base server a custom MCP server extends
+///
+public class CustomMcpBaseServer
+{
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ [JsonPropertyName("totalAvailableTools")]
+ public int TotalAvailableTools { get; set; }
+}
+
+///
+/// A tool included in a custom MCP server
+///
+public class CustomMcpTool
+{
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("displayName")]
+ public string? DisplayName { get; set; }
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ [JsonPropertyName("isEnabled")]
+ public bool IsEnabled { get; set; }
+
+ [JsonPropertyName("displayOrder")]
+ public int DisplayOrder { get; set; }
+}
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs
index 47f6c583..9a241c78 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs
@@ -17,6 +17,8 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Services;
///
public class Agent365ToolingService : IAgent365ToolingService
{
+ private static readonly JsonSerializerOptions CaseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
+
private readonly IConfigService _configService;
private readonly AuthenticationService _authService;
private readonly ILogger _logger;
@@ -137,7 +139,8 @@ private string BuildAgent365ToolsBaseUrl(string environment)
// Get from ConfigConstants to leverage existing URL construction logic
var discoverUrl = ConfigConstants.GetDiscoverEndpointUrl(environment);
var uri = new Uri(discoverUrl);
- return $"{uri.Scheme}://{uri.Host}";
+ // return $"{uri.Scheme}://{uri.Host}";
+ return "http://localhost:52857";
}
///
@@ -346,13 +349,8 @@ private string BuildGetMCPServerUrl(string environment)
return null;
}
- var options = new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- };
-
var serversResponse = JsonDeserializationHelper.DeserializeWithDoubleSerialization(
- responseContent, _logger, options);
+ responseContent, _logger, CaseInsensitiveOptions);
return serversResponse;
}
@@ -641,6 +639,139 @@ public async Task BlockServerAsync(
}
}
+ ///
+ /// Builds URL for the MCPManagement server endpoint.
+ /// Uses environment-scoped route when environmentId is provided so the server
+ /// can extract it from the route via RequestContextExtractor.
+ ///
+ private string BuildMcpManagementUrl(string environment, string? environmentId = null)
+ {
+ var baseUrl = BuildAgent365ToolsBaseUrl(environment);
+ if (!string.IsNullOrWhiteSpace(environmentId))
+ return $"{baseUrl}/mcp/environments/{environmentId}/servers/MCPManagement";
+ return $"{baseUrl}/agents/servers/MCPManagement";
+ }
+
+ ///
+ public async Task CreateCustomMcpServerAsync(
+ CreateCustomMcpServerRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ var endpointUrl = BuildMcpManagementUrl(_environment, request.EnvironmentId);
+ var correlationId = Internal.HttpClientFactory.GenerateCorrelationId();
+
+ _logger.LogInformation("Creating custom MCP server '{Name}' (CorrelationId: {CorrelationId})", request.Name, correlationId);
+ _logger.LogInformation("Environment: {Env}", _environment);
+ _logger.LogInformation("Endpoint URL: {Url}", endpointUrl);
+
+ var audience = ConfigConstants.GetAgent365ToolsResourceAppId(_environment);
+ _logger.LogInformation("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");
+ return null;
+ }
+
+ using var httpClient = Internal.HttpClientFactory.CreateAuthenticatedClient(authToken, correlationId: correlationId);
+
+ var requestObject = new
+ {
+ @params = new
+ {
+ name = "CreateCustomMCPServer",
+ arguments = request
+ },
+ method = "tools/call",
+ id = "1",
+ jsonrpc = "2.0"
+ };
+
+ var json = JsonSerializer.Serialize(requestObject);
+ LogRequest("POST", endpointUrl, json);
+
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Post, endpointUrl)
+ {
+ Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
+ };
+ httpRequest.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
+ httpRequest.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
+
+ using var response = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogError("Failed to create custom MCP server. Status: {Status}", response.StatusCode);
+ var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
+ _logger.LogError("Error response: {Error}", errorContent);
+ return null;
+ }
+
+ var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
+ _logger.LogInformation("Successfully received response from MCPManagement endpoint");
+ _logger.LogDebug("Raw MCPManagement response: {Response}", responseContent);
+
+ // Parse SSE response: join all "data: ..." lines
+ 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 envelope
+ var root = JsonNode.Parse(dataJson);
+ if (root == null)
+ {
+ _logger.LogError("Failed to parse MCPManagement response as JSON");
+ return null;
+ }
+
+ var rpcResult = root["result"];
+ var isError = rpcResult?["isError"]?.GetValue() ?? false;
+
+ var content = rpcResult?["content"]?.AsArray();
+ if (content == null)
+ {
+ _logger.LogError("Missing result.content in MCPManagement response");
+ return null;
+ }
+
+ if (isError)
+ {
+ var errorText = content
+ .Select(n => n?["text"]?.GetValue())
+ .FirstOrDefault(t => !string.IsNullOrWhiteSpace(t));
+ _logger.LogError("MCPManagement returned an error: {Error}", errorText ?? "(no error text)");
+ return null;
+ }
+
+ // 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("{"));
+
+ if (innerJson == null)
+ {
+ _logger.LogError("Inner JSON not found in MCPManagement response content");
+ return null;
+ }
+
+ var result = JsonSerializer.Deserialize(innerJson, CaseInsensitiveOptions);
+
+ if (result == null)
+ {
+ _logger.LogError("Failed to deserialize CreateCustomMCPServer response");
+ return null;
+ }
+
+ _logger.LogInformation("Successfully created custom MCP server '{Name}' with ID '{Id}'", result.Name, result.Id);
+ return result;
+ }
+
///
public async Task GetServerInfoAsync(string serverName, CancellationToken cancellationToken = default)
{
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs
index 605310b5..a2ae1b21 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/IAgent365ToolingService.cs
@@ -82,5 +82,15 @@ Task BlockServerAsync(
public Task GetServerInfoAsync(
string serverName,
CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a custom MCP server via the MCPManagement server
+ ///
+ /// Request with name, base server, tools, and optional metadata
+ /// Cancellation token
+ /// The created custom MCP server details
+ Task CreateCustomMcpServerAsync(
+ CreateCustomMcpServerRequest request,
+ CancellationToken cancellationToken = default);
}
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 f0a62e12..b33790a0 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,18 +41,19 @@ public void CreateCommand_HasAllExpectedSubcommands()
var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService);
// Assert
- command.Subcommands.Should().HaveCount(7);
-
+ command.Subcommands.Should().HaveCount(8);
+
var subcommandNames = command.Subcommands.Select(sc => sc.Name).ToList();
- subcommandNames.Should().Contain(new[]
- {
- "list-environments",
- "list-servers",
- "publish",
- "unpublish",
- "approve",
+ subcommandNames.Should().Contain(new[]
+ {
+ "list-environments",
+ "list-servers",
+ "publish",
+ "unpublish",
+ "approve",
"block",
- "package-mcp-server"
+ "package-mcp-server",
+ "create-custom-server"
});
}
@@ -303,7 +304,43 @@ public void CriticalOptions_HaveConsistentAliases(string subcommandName, string
$"Option '{optionName}' in '{subcommandName}' should have alias '{expectedAlias}'");
}
- [Fact]
+ [Fact]
+ public void CreateCustomServerSubcommand_HasCorrectOptions()
+ {
+ // Act
+ var command = DevelopMcpCommand.CreateCommand(_mockLogger, _mockToolingService);
+ var subcommand = command.Subcommands.First(sc => sc.Name == "create-custom-server");
+
+ // Assert
+ subcommand.Description.Should().Be("Create a custom MCP server via the MCPManagement server");
+
+ var options = subcommand.Options.ToList();
+ var optionNames = options.Select(o => o.Name).ToList();
+
+ optionNames.Should().Contain("name");
+ optionNames.Should().Contain("base-server-id");
+ optionNames.Should().Contain("display-name");
+ optionNames.Should().Contain("description");
+ optionNames.Should().Contain("instructions");
+ optionNames.Should().Contain("selected-base-tools");
+ optionNames.Should().Contain("environment-id");
+ optionNames.Should().Contain("dry-run");
+ optionNames.Should().Contain("config");
+
+ options.First(o => o.Name == "name").IsRequired.Should().BeTrue();
+ options.First(o => o.Name == "base-server-id").IsRequired.Should().BeTrue();
+
+ var displayNameOption = options.First(o => o.Name == "display-name");
+ displayNameOption.Aliases.Should().Contain("-d");
+
+ var envOption = options.First(o => o.Name == "environment-id");
+ envOption.Aliases.Should().Contain("-e");
+
+ var configOption = options.First(o => o.Name == "config");
+ configOption.Aliases.Should().Contain("-c");
+ }
+
+ [Fact]
public void NoSubcommands_UsePositionalArguments_OnlyOptions()
{
// This is a regression test to ensure we don't accidentally revert to positional arguments