From 84cf08506c67746c27717e5b412c5ee706a7848d Mon Sep 17 00:00:00 2001 From: Deepali Garg Date: Fri, 10 Apr 2026 13:39:09 -0700 Subject: [PATCH 1/2] Adding subcommands to create custom mcp server --- .../Commands/DevelopMcpCommand.cs | 119 +++++++++++++++++ .../Models/CreateCustomMcpServerRequest.cs | 90 +++++++++++++ .../Models/CreateCustomMcpServerResponse.cs | 105 +++++++++++++++ .../Services/Agent365ToolingService.cs | 125 +++++++++++++++++- .../Services/IAgent365ToolingService.cs | 10 ++ .../Commands/DevelopMcpCommandTests.cs | 59 +++++++-- 6 files changed, 491 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs create mode 100644 src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerResponse.cs diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs index 41ff6afe..eb26bd35 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 ? 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/Models/CreateCustomMcpServerRequest.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs new file mode 100644 index 00000000..41e2ef00 --- /dev/null +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs @@ -0,0 +1,90 @@ +// 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 + /// + [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..abd7a03d 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; @@ -346,13 +348,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 +638,122 @@ public async Task BlockServerAsync( } } + /// + /// Builds URL for the MCPManagement server endpoint + /// + private string BuildMcpManagementUrl(string environment) + { + var baseUrl = BuildAgent365ToolsBaseUrl(environment); + 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); + 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"); + + // 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 content = root["result"]?["content"]?.AsArray(); + if (content == null) + { + _logger.LogError("Missing result.content in MCPManagement response"); + 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 From 9ed4c99b18418e060d6a8cccc609277e2e1ccdd8 Mon Sep 17 00:00:00 2001 From: Deepali Garg Date: Mon, 13 Apr 2026 21:11:08 -0700 Subject: [PATCH 2/2] Fix create-custom-server command for MCPManagement compatibility - Use environment-scoped URL (/mcp/environments/{id}/servers/MCPManagement) so RequestContextExtractor can resolve the environment from the route - Serialize selectedBaseTools as comma-separated string to match MCPManagement server's expected format (was incorrectly sending as JSON array) - Change SelectedBaseTools type from string[]? to string? in request model - Improve error handling: surface isError=true responses from MCPManagement instead of silently failing with "Inner JSON not found" - Add debug logging of raw MCPManagement response Co-Authored-By: Claude Sonnet 4.6 --- .../Commands/DevelopMcpCommand.cs | 2 +- .../Constants/McpConstants.cs | 5 ++-- .../Models/CreateCustomMcpServerRequest.cs | 5 ++-- .../Services/Agent365ToolingService.cs | 28 +++++++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs index eb26bd35..d45982f2 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopMcpCommand.cs @@ -884,7 +884,7 @@ private static Command CreateCustomServerSubcommand(ILogger logger, IAgent365Too DisplayName = displayName, Description = description, Instructions = instructions, - SelectedBaseTools = selectedBaseTools?.Length > 0 ? selectedBaseTools : null, + SelectedBaseTools = selectedBaseTools?.Length > 0 ? string.Join(",", selectedBaseTools) : null, EnvironmentId = string.IsNullOrWhiteSpace(environmentId) ? null : environmentId }; 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 index 41e2ef00..f47cd490 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/CreateCustomMcpServerRequest.cs @@ -41,10 +41,11 @@ public class CreateCustomMcpServerRequest public required string BaseServerId { get; set; } /// - /// Optional subset of tools to select from the base server + /// 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; } + public string? SelectedBaseTools { get; set; } /// /// Optional additional tools from other sources (Graph, Connector, etc.) diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs index abd7a03d..9a241c78 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/Agent365ToolingService.cs @@ -139,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"; } /// @@ -639,11 +640,15 @@ public async Task BlockServerAsync( } /// - /// Builds URL for the MCPManagement server endpoint + /// 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) + 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"; } @@ -655,7 +660,7 @@ private string BuildMcpManagementUrl(string environment) if (request == null) throw new ArgumentNullException(nameof(request)); - var endpointUrl = BuildMcpManagementUrl(_environment); + var endpointUrl = BuildMcpManagementUrl(_environment, request.EnvironmentId); var correlationId = Internal.HttpClientFactory.GenerateCorrelationId(); _logger.LogInformation("Creating custom MCP server '{Name}' (CorrelationId: {CorrelationId})", request.Name, correlationId); @@ -709,6 +714,7 @@ private string BuildMcpManagementUrl(string environment) 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( @@ -724,13 +730,25 @@ private string BuildMcpManagementUrl(string environment) return null; } - var content = root["result"]?["content"]?.AsArray(); + 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())