Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
35 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
eb44439
Only enable destructive actions if explicitly set
WilliamBZA Apr 5, 2026
7c71b5d
Don't use backing field
WilliamBZA Apr 5, 2026
72996f4
Don't load tools from assembly
WilliamBZA Apr 5, 2026
83fb9b5
Add MCP environment data provider
johnsimons Apr 8, 2026
456b966
Forgot to register it!
johnsimons Apr 9, 2026
732568a
Merge pull request #5413 from Particular/john/data
johnsimons Apr 9, 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
Expand Up @@ -125,7 +125,7 @@ async Task InitializeServiceControl(ScenarioContext context)
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControl(settings, configuration);
hostBuilder.AddServiceControlHttps(settings.HttpsSettings);
hostBuilder.AddServiceControlApi(settings.CorsSettings);
hostBuilder.AddServiceControlApi(settings);

hostBuilder.AddServiceControlTesting(settings);

Expand All @@ -135,7 +135,7 @@ async Task InitializeServiceControl(ScenarioContext context)

host.UseTestRemoteIp();
host.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled);
host.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings);
host.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer);
Comment thread
danielmarbach marked this conversation as resolved.
Outdated
await host.StartAsync();
DomainEvents = host.Services.GetRequiredService<IDomainEvents>();
// Bring this back and look into the base address of the client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ async Task InitializeServiceControl(ScenarioContext context)
return criticalErrorContext.Stop(cancellationToken);
}, settings, configuration);

hostBuilder.AddServiceControlAuditApi(settings.CorsSettings);
hostBuilder.AddServiceControlAuditApi(settings);
hostBuilder.AddServiceControlHttps(settings.HttpsSettings);

hostBuilder.AddServiceControlAuditTesting(settings);
Expand All @@ -144,7 +144,7 @@ async Task InitializeServiceControl(ScenarioContext context)

host.UseTestRemoteIp();
host.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled);
host.UseServiceControlAudit(settings.ForwardedHeadersSettings, settings.HttpsSettings);
host.UseServiceControlAudit(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer);
await host.StartAsync();
ServiceProvider = host.Services;
InstanceTestServer = host.GetTestServer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@
"ServiceControlQueueAddress": "Particular.ServiceControl",
"TimeToRestartAuditIngestionAfterFailure": "00:01:00",
"EnableFullTextSearchOnBodies": true,
"EnableMcpServer": false,
"ShutdownTimeout": "00:00:05"
}
2 changes: 2 additions & 0 deletions src/ServiceControl.Audit/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ These settings are only here so that we can debug ServiceControl while developin
<add key="ServiceControl.Audit/ServiceControlQueueAddress" value="Particular.ServiceControl" />
<add key="ServiceControl.Audit/HostName" value="localhost" />
<add key="ServiceControl.Audit/DatabaseMaintenancePort" value="44445" />
<add key="ServiceControl.Audit/EnableMcpServer" value="false"/>

<!-- DEVS - Pick a transport to run Auditing instance on -->
<add key="ServiceControl.Audit/TransportType" value="LearningTransport" />
<!--<add key="ServiceControl.Audit/TransportType" value="AmazonSQS" />-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public override async Task Execute(HostArguments args, Settings settings)
//Do nothing. The transports in NSB 8 are designed to handle broker outages. Audit ingestion will be paused when broker is unavailable.
return Task.CompletedTask;
}, settings, endpointConfiguration);
hostBuilder.AddServiceControlAuditApi(settings.CorsSettings);
hostBuilder.AddServiceControlAuditApi(settings);

var app = hostBuilder.Build();
app.UseServiceControlAudit(settings.ForwardedHeadersSettings, settings.HttpsSettings);
app.UseServiceControlAudit(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer);
app.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled);

await app.RunAsync(settings.RootUrl);
Expand Down
3 changes: 3 additions & 0 deletions src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public Settings(string transportType = null, string persisterType = null, Loggin
ServiceControlQueueAddress = SettingsReader.Read<string>(SettingsRootNamespace, "ServiceControlQueueAddress");
TimeToRestartAuditIngestionAfterFailure = GetTimeToRestartAuditIngestionAfterFailure();
EnableFullTextSearchOnBodies = SettingsReader.Read(SettingsRootNamespace, "EnableFullTextSearchOnBodies", true);
EnableMcpServer = SettingsReader.Read(SettingsRootNamespace, "EnableMcpServer", false);
ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout);

AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath);
Expand Down Expand Up @@ -187,6 +188,8 @@ public int MaxBodySizeToStore

public bool EnableFullTextSearchOnBodies { get; set; }

public bool EnableMcpServer { get; set; }

// The default value is set to the maximum allowed time by the most
// restrictive hosting platform, which is Linux containers. Linux
// containers allow for a maximum of 10 seconds. We set it to 5 to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.AspNetCore;
using ServiceControl.Infrastructure;

static class HostApplicationBuilderExtensions
{
public static void AddServiceControlAuditApi(this IHostApplicationBuilder builder, CorsSettings corsSettings)
public static void AddServiceControlAuditApi(this IHostApplicationBuilder builder, Settings.Settings settings)
{
builder.Services.AddCors(options => options.AddDefaultPolicy(Cors.GetDefaultPolicy(corsSettings)));
if (settings.EnableMcpServer)
{
builder.Services
.AddMcpServer()
.WithHttpTransport()
Comment thread
WilliamBZA marked this conversation as resolved.
.WithToolsFromAssembly();
}

builder.Services.AddCors(options => options.AddDefaultPolicy(Cors.GetDefaultPolicy(settings.CorsSettings)));

// We're not explicitly adding Gzip here because it's already in the default list of supported compressors
builder.Services.AddResponseCompression();
Expand Down
147 changes: 147 additions & 0 deletions src/ServiceControl.Audit/Mcp/AuditMessageTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#nullable enable

namespace ServiceControl.Audit.Mcp;

using System.ComponentModel;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Infrastructure;
using ModelContextProtocol.Server;
using Persistence;

[McpServerToolType]
Comment thread
danielmarbach marked this conversation as resolved.
Outdated
public class AuditMessageTools(IAuditDataStore store)
{
[McpServerTool, Description("Get a list of successfully processed audit messages. Supports paging and sorting. Returns message metadata including endpoints, timing information, and message type.")]
Comment thread
danielmarbach marked this conversation as resolved.
Outdated
public async Task<string> GetAuditMessages(
[Description("Whether to include system messages in results. Default is false")] bool includeSystemMessages = false,
[Description("Page number (1-based). Default is 1")] int page = 1,
[Description("Number of results per page. Default is 50")] int perPage = 50,
[Description("Sort field: time_sent, processed_at, message_type, critical_time, delivery_time, processing_time. Default is time_sent")] string sort = "time_sent",
[Description("Sort direction: asc or desc. Default is desc")] string direction = "desc",
[Description("Filter by time sent start (ISO 8601 format)")] string? timeSentFrom = null,
[Description("Filter by time sent end (ISO 8601 format)")] string? timeSentTo = null,
CancellationToken cancellationToken = default)
{
var pagingInfo = new PagingInfo(page, perPage);
var sortInfo = new SortInfo(sort, direction);
var timeSentRange = new DateTimeRange(timeSentFrom, timeSentTo);

var results = await store.GetMessages(includeSystemMessages, pagingInfo, sortInfo, timeSentRange, cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}

[McpServerTool, Description("Search audit messages by a keyword or phrase. Searches across message content and metadata.")]
Comment thread
danielmarbach marked this conversation as resolved.
Outdated
public async Task<string> SearchAuditMessages(
[Description("The search query string")] string query,
[Description("Page number (1-based). Default is 1")] int page = 1,
[Description("Number of results per page. Default is 50")] int perPage = 50,
[Description("Sort field: time_sent, processed_at, message_type, critical_time, delivery_time, processing_time. Default is time_sent")] string sort = "time_sent",
[Description("Sort direction: asc or desc. Default is desc")] string direction = "desc",
[Description("Filter by time sent start (ISO 8601 format)")] string? timeSentFrom = null,
[Description("Filter by time sent end (ISO 8601 format)")] string? timeSentTo = null,
CancellationToken cancellationToken = default)
{
var pagingInfo = new PagingInfo(page, perPage);
var sortInfo = new SortInfo(sort, direction);
var timeSentRange = new DateTimeRange(timeSentFrom, timeSentTo);

var results = await store.QueryMessages(query, pagingInfo, sortInfo, timeSentRange, cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}

[McpServerTool, Description("Get audit messages received by a specific endpoint. Can optionally filter by keyword.")]
public async Task<string> GetAuditMessagesByEndpoint(
[Description("The name of the receiving endpoint")] string endpointName,
[Description("Optional keyword to filter messages")] string? keyword = null,
[Description("Whether to include system messages in results. Default is false")] bool includeSystemMessages = false,
[Description("Page number (1-based). Default is 1")] int page = 1,
[Description("Number of results per page. Default is 50")] int perPage = 50,
[Description("Sort field: time_sent, processed_at, message_type, critical_time, delivery_time, processing_time. Default is time_sent")] string sort = "time_sent",
[Description("Sort direction: asc or desc. Default is desc")] string direction = "desc",
[Description("Filter by time sent start (ISO 8601 format)")] string? timeSentFrom = null,
[Description("Filter by time sent end (ISO 8601 format)")] string? timeSentTo = null,
CancellationToken cancellationToken = default)
{
var pagingInfo = new PagingInfo(page, perPage);
var sortInfo = new SortInfo(sort, direction);
var timeSentRange = new DateTimeRange(timeSentFrom, timeSentTo);

var results = keyword != null
? await store.QueryMessagesByReceivingEndpointAndKeyword(endpointName, keyword, pagingInfo, sortInfo, timeSentRange, cancellationToken)
: await store.QueryMessagesByReceivingEndpoint(includeSystemMessages, endpointName, pagingInfo, sortInfo, timeSentRange, cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}

[McpServerTool, Description("Get all audit messages that belong to a specific conversation. A conversation groups related messages that were triggered by the same initial message.")]
public async Task<string> GetAuditMessagesByConversation(
[Description("The conversation ID to filter by")] string conversationId,
[Description("Page number (1-based). Default is 1")] int page = 1,
[Description("Number of results per page. Default is 50")] int perPage = 50,
[Description("Sort field: time_sent, processed_at, message_type, critical_time, delivery_time, processing_time. Default is time_sent")] string sort = "time_sent",
[Description("Sort direction: asc or desc. Default is desc")] string direction = "desc",
CancellationToken cancellationToken = default)
{
var pagingInfo = new PagingInfo(page, perPage);
var sortInfo = new SortInfo(sort, direction);

var results = await store.QueryMessagesByConversationId(conversationId, pagingInfo, sortInfo, cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}

[McpServerTool, Description("Get the body content of a specific audit message by its message ID.")]
public async Task<string> GetAuditMessageBody(
[Description("The message ID")] string messageId,
CancellationToken cancellationToken = default)
{
var result = await store.GetMessageBody(messageId, cancellationToken);

if (!result.Found)
{
return JsonSerializer.Serialize(new { Error = $"Message '{messageId}' not found." }, McpJsonOptions.Default);
}

if (!result.HasContent)
{
return JsonSerializer.Serialize(new { Error = $"Message '{messageId}' has no body content." }, McpJsonOptions.Default);
}

if (result.StringContent != null)
{
return JsonSerializer.Serialize(new
{
result.ContentType,
result.ContentLength,
Body = result.StringContent
}, McpJsonOptions.Default);
}

return JsonSerializer.Serialize(new
{
result.ContentType,
result.ContentLength,
Body = "(stream content - not available as text)"
}, McpJsonOptions.Default);
}
}
40 changes: 40 additions & 0 deletions src/ServiceControl.Audit/Mcp/EndpointTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#nullable enable

namespace ServiceControl.Audit.Mcp;

using System.ComponentModel;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ModelContextProtocol.Server;
using Persistence;

[McpServerToolType]
public class EndpointTools(IAuditDataStore store)
{
[McpServerTool, Description("Get a list of all known endpoints that have sent or received audit messages.")]
public async Task<string> GetKnownEndpoints(CancellationToken cancellationToken = default)
{
var results = await store.QueryKnownEndpoints(cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}

[McpServerTool, Description("Get audit message counts per day for a specific endpoint. Useful for understanding message throughput.")]
public async Task<string> GetEndpointAuditCounts(
[Description("The name of the endpoint")] string endpointName,
CancellationToken cancellationToken = default)
{
var results = await store.QueryAuditCounts(endpointName, cancellationToken);

return JsonSerializer.Serialize(new
{
results.QueryStats.TotalCount,
results.Results
}, McpJsonOptions.Default);
}
}
16 changes: 16 additions & 0 deletions src/ServiceControl.Audit/Mcp/McpJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#nullable enable

namespace ServiceControl.Audit.Mcp;

using System.Text.Json;
using System.Text.Json.Serialization;

static class McpJsonOptions
{
public static JsonSerializerOptions Default { get; } = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
}
1 change: 1 addition & 0 deletions src/ServiceControl.Audit/ServiceControl.Audit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="ByteSize" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" />
<PackageReference Include="ModelContextProtocol.AspNetCore" />
<PackageReference Include="NServiceBus.CustomChecks" />
<PackageReference Include="NServiceBus.Extensions.Hosting" />
<PackageReference Include="NServiceBus.Extensions.Logging" />
Expand Down
7 changes: 6 additions & 1 deletion src/ServiceControl.Audit/WebApplicationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ServiceControl.Audit;

public static class WebApplicationExtensions
{
public static void UseServiceControlAudit(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings)
public static void UseServiceControlAudit(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings, bool enableMcpServer)
{
app.UseServiceControlForwardedHeaders(forwardedHeadersSettings);
app.UseServiceControlHttps(httpsSettings);
Expand All @@ -17,5 +17,10 @@ public static void UseServiceControlAudit(this WebApplication app, ForwardedHead
app.UseHttpLogging();
app.UseCors();
app.MapControllers();

if (enableMcpServer)
{
app.MapMcp("/mcp");
Comment thread
danielmarbach marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"NotificationsFilter": null,
"AllowMessageEditing": false,
"EnableMcpServer": false,
"EnableIntegratedServicePulse": false,
"ServicePulseSettings": null,
"MessageFilter": null,
Expand Down
2 changes: 2 additions & 0 deletions src/ServiceControl/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ These settings are only here so that we can debug ServiceControl while developin
-->
<configuration>
<appSettings>
<add key="ServiceControl/EnableMcpServer" value="false"/>

<add key="ServiceControl/ForwardErrorMessages" value="false" />
<add key="ServiceControl/ErrorRetentionPeriod" value="10.00:00:00" />
<add key="ServiceControl/RemoteInstances" value="[{&quot;api_uri&quot;:&quot;http://localhost:44444/api/&quot;}]" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public override async Task Execute(HostArguments args, Settings settings)

var hostBuilder = Host.CreateApplicationBuilder();
hostBuilder.AddServiceControl(settings, endpointConfiguration);
hostBuilder.AddServiceControlApi(settings.CorsSettings);
hostBuilder.AddServiceControlApi(settings);

using var app = hostBuilder.Build();
await app.StartAsync();
Expand Down
4 changes: 2 additions & 2 deletions src/ServiceControl/Hosting/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ public override async Task Execute(HostArguments args, Settings settings)
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControlHttps(settings.HttpsSettings);
hostBuilder.AddServiceControl(settings, endpointConfiguration);
hostBuilder.AddServiceControlApi(settings.CorsSettings);
hostBuilder.AddServiceControlApi(settings);

var app = hostBuilder.Build();
app.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings);
app.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer);
if (settings.EnableIntegratedServicePulse)
{
app.UseServicePulse(settings.ServicePulseSettings);
Expand Down
Loading
Loading