-
Notifications
You must be signed in to change notification settings - Fork 16
U/deepaligarg/addcustommcp #368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
|
||
| /// <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 }; | ||
| 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"); | ||
|
||
|
|
||
| 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); | ||
|
Comment on lines
+916
to
+917
|
||
|
|
||
| return command; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates and sanitizes user input following Azure CLI security patterns | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
|
|
||
| /// <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"; | ||
|
||
|
|
||
| /// <summary> | ||
| /// Name of the tooling manifest file | ||
|
|
||
| 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; } | ||
| } |
There was a problem hiding this comment.
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[]withAllowMultipleArgumentsPerToken=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).