Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 34 additions & 152 deletions src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static Command CreateCommand(
// Add subcommands
developMcpCommand.AddCommand(CreateListEnvironmentsSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreateListServersSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreatePublishSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreatePublishSubcommand(logger, toolingService, graphApiService));
developMcpCommand.AddCommand(CreateUnpublishSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreateApproveSubcommand(logger, toolingService));
developMcpCommand.AddCommand(CreateBlockSubcommand(logger, toolingService));
Expand Down Expand Up @@ -279,180 +279,62 @@ private static Command CreateListServersSubcommand(
/// Creates the publish subcommand
/// </summary>
private static Command CreatePublishSubcommand(
ILogger logger,
IAgent365ToolingService toolingService)
ILogger logger,
IAgent365ToolingService toolingService,
GraphApiService? graphApiService)
{
var command = new Command("publish", "Publish an MCP server to a Dataverse environment");
var command = new Command("publish", "Publish an MCP server to a Dataverse environment. Creates the A365 Proxy + Public Clients Entra apps in your tenant, calls the platform publish endpoint, and back-fills redirect URIs and PPMI scope grants — same orchestration shape as register-external-mcp-server.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave the description as is. No need to share these details to external users.


var envIdOption = new Option<string?>(
["--environment-id", "-e"],
description: "Dataverse environment ID"
);
envIdOption.IsRequired = false; // Allow null so we can prompt
description: "Dataverse environment ID");
command.AddOption(envIdOption);

var serverNameOption = new Option<string?>(
["--server-name", "-s"],
description: "MCP server name to publish"
);
serverNameOption.IsRequired = false; // Allow null so we can prompt
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why changing this? This is false so that users can be prompted for this right?

description: "MCP server name to publish");
command.AddOption(serverNameOption);

var aliasOption = new Option<string?>(
["--alias", "-a"],
description: "Alias for the MCP server"
);
description: "Alias for the MCP server (used as the MCC row name and the CMS connector id)");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. Internal details should not be shared with users

command.AddOption(aliasOption);

var displayNameOption = new Option<string?>(
["--display-name", "-d"],
description: "Display name for the MCP server"
);
description: "Display name for the MCP server (max 30 chars)");
command.AddOption(displayNameOption);

var dryRunOption = new Option<bool>(
name: "--dry-run",
description: "Show what would be done without executing"
);
command.AddOption(dryRunOption);

var verboseOption = new Option<bool>(
["--verbose", "-v"],
description: "Enable verbose logging"
);
command.AddOption(verboseOption);

command.SetHandler(async (envId, serverName, alias, displayName, dryRun, verbose) =>
{
_ = verbose;
try
{
// Validate and prompt for missing required arguments with security checks
if (string.IsNullOrWhiteSpace(envId))
{
envId = InputValidator.PromptAndValidateRequiredInput("Enter Dataverse environment ID: ", "Environment ID");
if (string.IsNullOrWhiteSpace(envId))
{
logger.LogError("Environment ID is required");
return;
}
}
else
{
// Validate provided environment ID
envId = InputValidator.ValidateInput(envId, "Environment ID");
if (envId == null)
{
logger.LogError("Invalid environment ID format");
return;
}
}

if (string.IsNullOrWhiteSpace(serverName))
{
serverName = InputValidator.PromptAndValidateRequiredInput("Enter MCP server name to publish: ", "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;
}
}

logger.LogInformation("Starting publish operation for server {ServerName} in environment {EnvId}...", serverName, envId);

if (dryRun)
{
logger.LogInformation("[DRY RUN] Would read config from a365.config.json");
logger.LogInformation("[DRY RUN] Would publish MCP server {ServerName} to environment {EnvId}", serverName, envId);
logger.LogInformation("[DRY RUN] Alias: {Alias}", alias ?? "[would prompt]");
logger.LogInformation("[DRY RUN] Display Name: {DisplayName}", displayName ?? "[would prompt]");
await Task.CompletedTask;
return;
}

// Validate and prompt for missing optional values with security checks
if (string.IsNullOrWhiteSpace(alias))
{
alias = InputValidator.PromptAndValidateRequiredInput("Enter alias for the MCP server: ", "Alias", 50);
if (string.IsNullOrWhiteSpace(alias))
{
logger.LogError("Alias is required");
return;
}
}
else
{
// Validate provided alias
alias = InputValidator.ValidateInput(alias, "Alias", maxLength: 50);
if (alias == null)
{
logger.LogError("Invalid alias format");
return;
}
}

if (string.IsNullOrWhiteSpace(displayName))
{
displayName = InputValidator.PromptAndValidateRequiredInput("Enter display name for the MCP server: ", "Display name", 100);
if (string.IsNullOrWhiteSpace(displayName))
{
logger.LogError("Display name is required");
return;
}
}
else
{
// Validate provided display name
displayName = InputValidator.ValidateInput(displayName, "Display name", maxLength: 100);
if (displayName == null)
{
logger.LogError("Invalid display name format");
return;
}
}
}
catch (ArgumentException ex)
{
logger.LogError("Input validation failed: {Message}", ex.Message);
return;
}
var tenantIdOption = new Option<string?>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed

["--tenant-id", "-t"],
description: "Entra tenant ID for Entra app creation (defaults to current az login tenant)");
command.AddOption(tenantIdOption);

// Create request
var request = new PublishMcpServerRequest
{
Alias = alias,
DisplayName = displayName
};
var serviceTreeIdOption = new Option<string?>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is definitely not needed. Users won't have serviceTreeId

"--service-tree-id",
description: "ServiceTree ID for Entra app registration (required in Microsoft corporate tenants)");
command.AddOption(serviceTreeIdOption);

// Call service
var response = await toolingService.PublishServerAsync(envId, serverName, request);
var dryRunOption = new Option<bool>("--dry-run", "Show what would be done without executing");
command.AddOption(dryRunOption);

if (response == null || !response.IsSuccess)
{
if (response?.Message != null)
{
logger.LogError("Failed to publish MCP server {ServerName} to environment {EnvId}: {ErrorMessage}", serverName, envId, response.Message);
}
else
{
logger.LogError("Failed to publish MCP server {ServerName} to environment {EnvId}: No response received", serverName, envId);
}
return;
}
// Verbose is handled globally in Program.cs (sets LogLevel.Debug); declared here so the parser accepts -v.
command.AddOption(new Option<bool>(["--verbose", "-v"], description: "Enable verbose logging"));

logger.LogInformation("Successfully published MCP server {ServerName} to environment {EnvId}", serverName, envId);
command.SetHandler(async (context) =>
{
var args = new RawPublishArgs(
EnvironmentId: context.ParseResult.GetValueForOption(envIdOption),
ServerName: context.ParseResult.GetValueForOption(serverNameOption),
Alias: context.ParseResult.GetValueForOption(aliasOption),
DisplayName: context.ParseResult.GetValueForOption(displayNameOption),
TenantId: context.ParseResult.GetValueForOption(tenantIdOption),
ServiceTreeId: context.ParseResult.GetValueForOption(serviceTreeIdOption),
DryRun: context.ParseResult.GetValueForOption(dryRunOption));

}, envIdOption, serverNameOption, aliasOption, displayNameOption, dryRunOption, verboseOption);
var executor = new PublishCommandExecutor(logger, toolingService, graphApiService);
await executor.ExecuteAsync(args, context.GetCancellationToken());
});

return command;
}
Expand Down
Loading
Loading