diff --git a/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpToolServerConfigurationServiceTests.cs b/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpToolServerConfigurationServiceTests.cs index 66bcb7d1..b94ed0ab 100644 --- a/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpToolServerConfigurationServiceTests.cs +++ b/src/Tests/Microsoft.Agents.A365.Tooling.Tests/Services/McpToolServerConfigurationServiceTests.cs @@ -833,6 +833,116 @@ public async Task SendChatHistoryAsync_HttpRequestExceptionWithStatusCode_Return httpEx.Should().NotBeNull(); httpEx!.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); } + + [Fact] + public async Task ListToolServersAsync_GatewayResponseWithInstructions_PopulatesInstructions() + { + // Arrange + var configMock = new Mock(); + configMock.Setup(c => c["MCP_PLATFORM_ENDPOINT"]).Returns("https://test.example.com"); + + const string expectedInstructions = "Use these tools to interact with Salesforce data. Always call GetTables first."; + var responseJson = JsonSerializer.Serialize(new + { + mcpServers = new[] + { + new + { + mcpServerName = "mcp_SalesforceServer", + url = "https://test.example.com/agents/servers/mcp_SalesforceServer", + id = "sf-1", + scope = "api://audience/Tools.ListInvoke.All", + audience = "api://audience", + publisher = "Microsoft", + instructions = expectedInstructions + } + } + }); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(() => new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }); + + using var httpClient = new HttpClient(mockHttpMessageHandler.Object); + var httpClientFactoryMock = new Mock(); + httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())) + .Returns(httpClient); + + var service = new McpToolServerConfigurationService( + _loggerMock.Object, + configMock.Object, + _serviceProviderMock.Object, + httpClientFactoryMock.Object); + + // Act + var servers = await service.ListToolServersAsync("agent-123", "token-456"); + + // Assert + servers.Should().ContainSingle(); + servers[0].mcpServerName.Should().Be("mcp_SalesforceServer"); + servers[0].instructions.Should().Be(expectedInstructions); + } + + [Fact] + public async Task ListToolServersAsync_GatewayResponseWithoutInstructions_LeavesInstructionsNull() + { + // Arrange + var configMock = new Mock(); + configMock.Setup(c => c["MCP_PLATFORM_ENDPOINT"]).Returns("https://test.example.com"); + + var responseJson = JsonSerializer.Serialize(new + { + mcpServers = new[] + { + new + { + mcpServerName = "mcp_MailServer", + url = "https://test.example.com/agents/servers/mcp_MailServer", + id = "mail-1" + } + } + }); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(() => new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }); + + using var httpClient = new HttpClient(mockHttpMessageHandler.Object); + var httpClientFactoryMock = new Mock(); + httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())) + .Returns(httpClient); + + var service = new McpToolServerConfigurationService( + _loggerMock.Object, + configMock.Object, + _serviceProviderMock.Object, + httpClientFactoryMock.Object); + + // Act + var servers = await service.ListToolServersAsync("agent-123", "token-456"); + + // Assert + servers.Should().ContainSingle(); + servers[0].instructions.Should().BeNull(); + } } } diff --git a/src/Tooling/Core/Models/MCPServerConfig.cs b/src/Tooling/Core/Models/MCPServerConfig.cs index eb65c727..8a1ba36e 100644 --- a/src/Tooling/Core/Models/MCPServerConfig.cs +++ b/src/Tooling/Core/Models/MCPServerConfig.cs @@ -42,6 +42,13 @@ public class MCPServerConfig /// public string? publisher { get; set; } + /// + /// Gets or sets optional natural-language instructions describing how an agent should use + /// this server's tools. Surfaced by the discovery response when the server declares them; + /// null when the server provides no instructions. + /// + public string? instructions { get; set; } + /// /// Gets or sets per-server HTTP headers, including the Authorization header populated /// by AttachPerAudienceTokensAsync before tool connections are established. diff --git a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs index 360c3c64..14e7fdf9 100644 --- a/src/Tooling/Core/Services/McpToolServerConfigurationService.cs +++ b/src/Tooling/Core/Services/McpToolServerConfigurationService.cs @@ -246,6 +246,7 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) string? scope = null; string? audience = null; string? publisher = null; + string? instructions = null; if (serverElement.TryGetProperty("mcpServerName", out var nameElement) && nameElement.ValueKind == JsonValueKind.String) @@ -284,6 +285,11 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) { publisher = publisherElement.GetString(); } + if (serverElement.TryGetProperty("instructions", out var instructionsElement) && + instructionsElement.ValueKind == JsonValueKind.String) + { + instructions = instructionsElement.GetString(); + } // Both Name and Endpoint are required if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(endpoint)) @@ -298,7 +304,8 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) id = id ?? string.Empty, scope = scope, audience = audience, - publisher = publisher + publisher = publisher, + instructions = instructions }; } catch (Exception) @@ -323,6 +330,7 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) string? scope = null; string? audience = null; string? publisher = null; + string? instructions = null; if (serverElement.TryGetProperty("mcpServerName", out var nameElement) && nameElement.ValueKind == JsonValueKind.String) @@ -361,6 +369,11 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) { publisher = publisherElement.GetString(); } + if (serverElement.TryGetProperty("instructions", out var instructionsElement) && + instructionsElement.ValueKind == JsonValueKind.String) + { + instructions = instructionsElement.GetString(); + } // Both Name and ServerName are required if (string.IsNullOrWhiteSpace(name)) @@ -378,7 +391,8 @@ JsonValueKind.Object when jsonDoc.TryGetProperty("mcpServers", out var servers) id = id ?? string.Empty, scope = scope, audience = audience, - publisher = publisher + publisher = publisher, + instructions = instructions }; } catch (Exception)