Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2189e04
Add failed message MCP server
WilliamBZA Mar 9, 2026
1c80991
Add feature flag check for MCP
WilliamBZA Mar 9, 2026
7b47a30
Update to v1.1.0 of ModelContextProtocol.AspNetCore
WilliamBZA Mar 9, 2026
5c5645e
Turn MCP off by default
WilliamBZA Mar 9, 2026
2c00d5e
Put packages in alphabetical order
WilliamBZA Mar 9, 2026
e4ab0ea
Update approvals
WilliamBZA Mar 9, 2026
4c8ecda
Don't pass the full settings object in
WilliamBZA Mar 9, 2026
add9235
Add failed message MCP server
WilliamBZA Mar 9, 2026
ae22f8a
Use /mcp as the route
WilliamBZA Mar 20, 2026
a160b4d
Add MCP for audit
WilliamBZA Mar 20, 2026
5da1181
Remove duplicate project reference
WilliamBZA Mar 20, 2026
49d946b
Update approvals
WilliamBZA Mar 20, 2026
312e2a7
Add test
WilliamBZA Mar 20, 2026
fbc8a00
Move to approvals
WilliamBZA Mar 23, 2026
481de0d
Move tests to use POCOs
WilliamBZA Mar 23, 2026
7e014b5
Move packages to be in alphabetical order
WilliamBZA Mar 23, 2026
9bf1cbc
Add unit tests for MCP
WilliamBZA Mar 23, 2026
fe4d19a
Add primary acceptance tests
WilliamBZA Mar 23, 2026
0efd5dc
Update MCP descriptions
WilliamBZA Mar 23, 2026
97b03b6
Add approval file
WilliamBZA Mar 23, 2026
a12b134
Update audit approval files
WilliamBZA Mar 23, 2026
92a4624
Update raven audit approval
WilliamBZA Mar 23, 2026
23165d4
Order approval files
WilliamBZA Mar 23, 2026
2a920c8
Add logging
WilliamBZA Mar 23, 2026
a788827
Improve MCP metadata guidance for AI clients (#5402)
danielmarbach Mar 25, 2026
06d9e45
Align wording order
danielmarbach Mar 25, 2026
5a43098
Return typed structured content for MCP tools while preserving text c…
danielmarbach Mar 26, 2026
537bc13
Add MCP source-generated JSON contexts
danielmarbach Mar 26, 2026
2959499
Removed unused options type
danielmarbach Mar 26, 2026
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
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="10.0.5" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="10.0.5" />
<PackageVersion Include="Validar.Fody" Version="1.9.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="1.1.0" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup Label="Versions to pin transitive references">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace ServiceControl.AcceptanceTesting.Mcp;

using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true)]
[JsonSerializable(typeof(List<JsonElement>))]
[JsonSerializable(typeof(McpListToolsResponse))]
[JsonSerializable(typeof(McpCallToolResponse))]
[JsonSerializable(typeof(McpInitializeResponse))]
public partial class McpAcceptanceJsonContext : JsonSerializerContext;
194 changes: 194 additions & 0 deletions src/ServiceControl.AcceptanceTesting/Mcp/McpAcceptanceTestSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
namespace ServiceControl.AcceptanceTesting.Mcp;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using NUnit.Framework;

public static class McpAcceptanceTestSupport
{
const string RequestedProtocolVersion = "2025-11-25";

public static async Task<HttpResponseMessage> InitializeMcpSession(HttpClient httpClient)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = JsonContent.Create(new
{
jsonrpc = "2.0",
id = 1,
method = "initialize",
@params = new
{
protocolVersion = RequestedProtocolVersion,
capabilities = new { },
clientInfo = new { name = "test-client", version = "1.0" }
}
})
};
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
request.Headers.Add("MCP-Protocol-Version", RequestedProtocolVersion);
return await httpClient.SendAsync(request);
}

public static async Task<McpSessionInfo> InitializeAndGetSessionInfo(HttpClient httpClient)
{
var response = await InitializeMcpSession(httpClient);
if (!response.IsSuccessStatusCode)
{
return null;
}

var initializeResponse = JsonSerializer.Deserialize(await ReadMcpResponseJson(response), McpAcceptanceJsonContext.Default.McpInitializeResponse)!;
var protocolVersion = initializeResponse.Result.ProtocolVersion;

if (!response.Headers.TryGetValues("mcp-session-id", out var values))
{
return null;
}

var sessionId = values.FirstOrDefault();
if (sessionId == null)
{
return null;
}

var initializedResponse = await SendInitializedNotification(httpClient, sessionId, protocolVersion);
if (!initializedResponse.IsSuccessStatusCode)
{
return null;
}

return new McpSessionInfo(sessionId, protocolVersion);
}

public static async Task<HttpResponseMessage> SendMcpRequest(HttpClient httpClient, McpSessionInfo sessionInfo, string method, object @params)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = JsonContent.Create(new
{
jsonrpc = "2.0",
id = 2,
method,
@params
})
};
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
request.Headers.Add("mcp-session-id", sessionInfo.SessionId);
request.Headers.Add("MCP-Protocol-Version", sessionInfo.ProtocolVersion);
return await httpClient.SendAsync(request);
}

public static async Task<string> ReadMcpResponseJson(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();
var contentType = response.Content.Headers.ContentType?.MediaType;

if (contentType == "text/event-stream")
{
foreach (var line in body.Split('\n'))
{
if (line.StartsWith("data: "))
{
return line.Substring("data: ".Length);
}
}
}

return body;
}

public static McpListToolsResponse DeserializeListToolsResponse(string toolsJson) =>
JsonSerializer.Deserialize(toolsJson, McpAcceptanceJsonContext.Default.McpListToolsResponse)!;

public static McpCallToolResponse DeserializeCallToolResponse(string toolResult) =>
JsonSerializer.Deserialize(toolResult, McpAcceptanceJsonContext.Default.McpCallToolResponse)!;

public static string FormatToolsForApproval(List<JsonElement> sortedTools) =>
JsonSerializer.Serialize(sortedTools, McpAcceptanceJsonContext.Default.ListJsonElement);

public static void AssertToolsHaveOutputSchema(IEnumerable<JsonElement> tools)
{
foreach (var tool in tools)
{
Assert.That(tool.TryGetProperty("outputSchema", out var outputSchema), Is.True, $"Tool '{tool.GetProperty("name").GetString()}' should expose outputSchema.");
Assert.That(outputSchema.ValueKind, Is.EqualTo(JsonValueKind.Object), $"Tool '{tool.GetProperty("name").GetString()}' should expose object outputSchema.");
}
}

public static void AssertStructuredToolResponse(string rawResponse, JsonElement structuredContent, IReadOnlyList<McpContent> content, Action<JsonElement> assertStructuredContent)
{
Assert.That(structuredContent.ValueKind, Is.EqualTo(JsonValueKind.Object), rawResponse);
assertStructuredContent(structuredContent);

Assert.That(content, Has.Count.GreaterThanOrEqualTo(1), rawResponse);
Assert.That(content[0].Type, Is.EqualTo("text"), rawResponse);
Assert.That(content[0].Text, Is.Not.Null.And.Not.Empty, rawResponse);

using var textPayload = JsonDocument.Parse(content[0].Text);
Assert.That(JsonElement.DeepEquals(structuredContent, textPayload.RootElement), Is.True, $"text content should serialize the structured payload. Raw response: {rawResponse}");
}

static async Task<HttpResponseMessage> SendInitializedNotification(HttpClient httpClient, string sessionId, string protocolVersion)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = JsonContent.Create(new
{
jsonrpc = "2.0",
method = "notifications/initialized"
})
};
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
request.Headers.Add("mcp-session-id", sessionId);
request.Headers.Add("MCP-Protocol-Version", protocolVersion);
return await httpClient.SendAsync(request);
}
}

public record McpSessionInfo(string SessionId, string ProtocolVersion);

public class McpListToolsResponse
{
public McpListToolsResult Result { get; set; }
}

public class McpListToolsResult
{
public List<object> Tools { get; set; } = [];
}

public class McpCallToolResponse
{
public McpCallToolResult Result { get; set; }
}

public class McpCallToolResult
{
public JsonElement StructuredContent { get; set; }
public List<McpContent> Content { get; set; } = [];
}

public class McpContent
{
public string Type { get; set; }
public string Text { get; set; }
}

public class McpInitializeResponse
{
public McpInitializeResult Result { get; set; }
}

public class McpInitializeResult
{
public string ProtocolVersion { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
<PackageReference Include="NServiceBus.AcceptanceTesting" />
<PackageReference Include="NServiceBus.Extensions.Logging" />
<PackageReference Include="NServiceBus.Persistence.NonDurable" />
<PackageReference Include="NUnit" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
</ItemGroup>

</Project>
</Project>
Loading
Loading