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
119 changes: 119 additions & 0 deletions src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -800,6 +801,124 @@ private static Command CreatePackageMCPServerSubCommand(ILogger logger, IAgent36
return command;
}

/// <summary>
/// Creates the create-custom-server subcommand
/// </summary>
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<string>("--name", "Unique logical name for the custom MCP server (no whitespace)") { IsRequired = true };
var baseServerIdOption = new Option<string>("--base-server-id", "Base server ID to extend (e.g. mcp_MailServer)") { IsRequired = true };
var displayNameOption = new Option<string?>(["--display-name", "-d"], description: "User-friendly display name");
var descriptionOption = new Option<string?>(["--description"], description: "Description of the custom MCP server");
var instructionsOption = new Option<string?>(["--instructions"], description: "AI agent instructions for how to use this server");
var selectedBaseToolsOption = new Option<string[]?>("--selected-base-tools", description: "Comma-separated list of tool names to select from the base server") { AllowMultipleArgumentsPerToken = true };
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The option is declared as string[] with AllowMultipleArgumentsPerToken=true, but the help text says 'Comma-separated list'. This is inconsistent for users and maintainers. Either (a) update the description to 'one or more tool names' (multiple values) or (b) accept a single string and split on commas (comma-separated).

Suggested change
var selectedBaseToolsOption = new Option<string[]?>("--selected-base-tools", description: "Comma-separated list of tool names to select from the base server") { AllowMultipleArgumentsPerToken = true };
var selectedBaseToolsOption = new Option<string[]?>("--selected-base-tools", description: "One or more tool names to select from the base server") { AllowMultipleArgumentsPerToken = true };

Copilot uses AI. Check for mistakes.
var environmentIdOption = new Option<string?>(["--environment-id", "-e"], description: "Dataverse environment ID (null = tenant-level)");
var dryRunOption = new Option<bool>("--dry-run", description: "Show what would be done without executing");
var configOption = new Option<string>(["-c", "--config"], getDefaultValue: () => "a365.config.json", description: "Configuration file path");
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The subcommand defines --config/-c but the handler signature and SetHandler bindings omit configOption, so the option has no effect (and users can’t influence which config file is used). Add a config parameter to the handler and include configOption in the SetHandler option list, then ensure the provided path is actually applied where configuration is loaded.

Copilot uses AI. Check for mistakes.

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) =>
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The subcommand defines --config/-c but the handler signature and SetHandler bindings omit configOption, so the option has no effect (and users can’t influence which config file is used). Add a config parameter to the handler and include configOption in the SetHandler option list, then ensure the provided path is actually applied where configuration is loaded.

Copilot uses AI. Check for mistakes.
{
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,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The option is declared as string[] with AllowMultipleArgumentsPerToken=true, but the help text says 'Comma-separated list'. This is inconsistent for users and maintainers. Either (a) update the description to 'one or more tool names' (multiple values) or (b) accept a single string and split on commas (comma-separated).

Copilot uses AI. Check for mistakes.
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);
Comment on lines +916 to +917
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The subcommand defines --config/-c but the handler signature and SetHandler bindings omit configOption, so the option has no effect (and users can’t influence which config file is used). Add a config parameter to the handler and include configOption in the SetHandler option list, then ensure the provided path is actually applied where configuration is loaded.

Copilot uses AI. Check for mistakes.

return command;
}

/// <summary>
/// Validates and sanitizes user input following Azure CLI security patterns
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment on lines +13 to +14
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Switching production constants to test values (and leaving the old values commented out) risks redirecting auth/resource targeting across all environments. Move these values to environment-specific configuration (or introduce separate constants per environment with explicit selection logic) and remove commented-out constants to avoid accidental shipping of test endpoints/IDs.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Agent 365 Tools identifier URI (used for admin consent URL construction).
/// </summary>
public const string Agent365ToolsIdentifierUri = "https://agent365.svc.cloud.microsoft";
public const string Agent365ToolsIdentifierUri = "https://test.agent365.svc.cloud.microsoft";
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Switching production constants to test values (and leaving the old values commented out) risks redirecting auth/resource targeting across all environments. Move these values to environment-specific configuration (or introduce separate constants per environment with explicit selection logic) and remove commented-out constants to avoid accidental shipping of test endpoints/IDs.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Name of the tooling manifest file
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Request model for creating a custom MCP server via the MCPManagement server
/// </summary>
public class CreateCustomMcpServerRequest
{
/// <summary>
/// Unique logical name for the custom MCP server (no whitespace)
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; set; }

/// <summary>
/// User-friendly display name
/// </summary>
[JsonPropertyName("displayName")]
public string? DisplayName { get; set; }

/// <summary>
/// Description of the custom MCP server
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>
/// AI agent instructions for how to use this server
/// </summary>
[JsonPropertyName("instructions")]
public string? Instructions { get; set; }

/// <summary>
/// Base server ID to extend (e.g. "mcp_MailServer")
/// </summary>
[JsonPropertyName("baseServerId")]
public required string BaseServerId { get; set; }

/// <summary>
/// Optional subset of tools to select from the base server.
/// Comma-separated string matching the MCPManagement server's expected format.
/// </summary>
[JsonPropertyName("selectedBaseTools")]
public string? SelectedBaseTools { get; set; }

/// <summary>
/// Optional additional tools from other sources (Graph, Connector, etc.)
/// </summary>
[JsonPropertyName("additionalTools")]
public AdditionalToolRequest[]? AdditionalTools { get; set; }

/// <summary>
/// null = tenant-level, value = environment-level
/// </summary>
[JsonPropertyName("environmentId")]
public string? EnvironmentId { get; set; }
}

/// <summary>
/// Represents an additional tool to include from a non-base-server source
/// </summary>
public class AdditionalToolRequest
{
/// <summary>
/// The type of backend tool (see BackendToolType enum values)
/// </summary>
[JsonPropertyName("backendToolType")]
public int BackendToolType { get; set; }

/// <summary>
/// Microsoft Graph operation ID (used when backendToolType = 2)
/// </summary>
[JsonPropertyName("graphOperationId")]
public string? GraphOperationId { get; set; }

/// <summary>
/// Power Platform connector ID (used when backendToolType = 1)
/// </summary>
[JsonPropertyName("connectorId")]
public string? ConnectorId { get; set; }

/// <summary>
/// Power Platform connector operation ID (used when backendToolType = 1)
/// </summary>
[JsonPropertyName("connectorOperationId")]
public string? ConnectorOperationId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Response model from the CreateCustomMCPServer MCPManagement tool call
/// </summary>
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; }
}

/// <summary>
/// Information about the base server a custom MCP server extends
/// </summary>
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; }
}

/// <summary>
/// A tool included in a custom MCP server
/// </summary>
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; }
}
Loading
Loading