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
Original file line number Diff line number Diff line change
Expand Up @@ -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<IConfiguration>();
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<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.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<IHttpClientFactory>();
httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
.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<IConfiguration>();
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<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.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<IHttpClientFactory>();
httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
.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();
}
}
}

7 changes: 7 additions & 0 deletions src/Tooling/Core/Models/MCPServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public class MCPServerConfig
/// </summary>
public string? publisher { get; set; }

/// <summary>
/// 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.
/// </summary>
public string? instructions { get; set; }

/// <summary>
/// Gets or sets per-server HTTP headers, including the Authorization header populated
/// by <c>AttachPerAudienceTokensAsync</c> before tool connections are established.
Expand Down
18 changes: 16 additions & 2 deletions src/Tooling/Core/Services/McpToolServerConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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();
}
Comment on lines +372 to +376

// Both Name and ServerName are required
if (string.IsNullOrWhiteSpace(name))
Expand All @@ -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)
Expand Down