From 779e6ed519aa2ecad027d950c53ef5c0aa9be513 Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 11:35:16 +0200 Subject: [PATCH 01/31] Add failed message MCP server --- src/Directory.Packages.props | 1 + src/ServiceControl/App.config | 2 + .../Infrastructure/Settings/Settings.cs | 3 + .../HostApplicationBuilderExtensions.cs | 10 ++ src/ServiceControl/Mcp/ArchiveTools.cs | 90 ++++++++++++++++++ src/ServiceControl/Mcp/FailedMessageTools.cs | 94 +++++++++++++++++++ src/ServiceControl/Mcp/FailureGroupTools.cs | 30 ++++++ src/ServiceControl/Mcp/McpJsonOptions.cs | 14 +++ src/ServiceControl/Mcp/RetryTools.cs | 84 +++++++++++++++++ .../Handlers/ArchiveMessageHandler.cs | 2 +- src/ServiceControl/ServiceControl.csproj | 1 + .../WebApplicationExtensions.cs | 3 + 12 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/ServiceControl/Mcp/ArchiveTools.cs create mode 100644 src/ServiceControl/Mcp/FailedMessageTools.cs create mode 100644 src/ServiceControl/Mcp/FailureGroupTools.cs create mode 100644 src/ServiceControl/Mcp/McpJsonOptions.cs create mode 100644 src/ServiceControl/Mcp/RetryTools.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fa54c02e52..d78bd8f04b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -83,6 +83,7 @@ + diff --git a/src/ServiceControl/App.config b/src/ServiceControl/App.config index d6271805e5..817bd95e11 100644 --- a/src/ServiceControl/App.config +++ b/src/ServiceControl/App.config @@ -5,6 +5,8 @@ These settings are only here so that we can debug ServiceControl while developin --> + + diff --git a/src/ServiceControl/Infrastructure/Settings/Settings.cs b/src/ServiceControl/Infrastructure/Settings/Settings.cs index d71b9dca66..24e7082863 100644 --- a/src/ServiceControl/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl/Infrastructure/Settings/Settings.cs @@ -81,6 +81,7 @@ public Settings( DisableExternalIntegrationsPublishing = SettingsReader.Read(SettingsRootNamespace, "DisableExternalIntegrationsPublishing", false); TrackInstancesInitialValue = SettingsReader.Read(SettingsRootNamespace, "TrackInstancesInitialValue", true); ShutdownTimeout = SettingsReader.Read(SettingsRootNamespace, "ShutdownTimeout", ShutdownTimeout); + EnableMcpServer = SettingsReader.Read(SettingsRootNamespace, "EnableMcpServer", false); AssemblyLoadContextResolver = static assemblyPath => new PluginAssemblyLoadContext(assemblyPath); } @@ -113,6 +114,8 @@ public Settings( public bool AllowMessageEditing { get; set; } + public bool EnableMcpServer { get; set; } + public bool EnableIntegratedServicePulse { get; set; } public ServicePulseSettings ServicePulseSettings { get; set; } diff --git a/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs b/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs index 298885ae0f..173ce94b70 100644 --- a/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs @@ -9,6 +9,8 @@ using Microsoft.Extensions.Hosting; using Particular.LicensingComponent.WebApi; using Particular.ServiceControl; + using ServiceBus.Management.Infrastructure.Settings; + using ServiceControl.Configuration; static class HostApplicationBuilderExtensions { @@ -20,6 +22,14 @@ public static void AddServiceControlApi(this IHostApplicationBuilder builder, Co builder.AddServiceControlApis(); + if (SettingsReader.Read(Settings.SettingsRootNamespace, "EnableMcpServer", false)) + { + builder.Services + .AddMcpServer() + .WithHttpTransport() + .WithToolsFromAssembly(); + } + builder.Services.AddCors(options => options.AddDefaultPolicy(Cors.GetDefaultPolicy(corsSettings))); // We're not explicitly adding Gzip here because it's already in the default list of supported compressors diff --git a/src/ServiceControl/Mcp/ArchiveTools.cs b/src/ServiceControl/Mcp/ArchiveTools.cs new file mode 100644 index 0000000000..86abe21de0 --- /dev/null +++ b/src/ServiceControl/Mcp/ArchiveTools.cs @@ -0,0 +1,90 @@ +namespace ServiceControl.Mcp; + +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using MessageFailures.InternalMessages; +using ModelContextProtocol.Server; +using NServiceBus; +using Persistence.Recoverability; +using ServiceControl.Recoverability; + +[McpServerToolType] +public class ArchiveTools(IMessageSession messageSession, IArchiveMessages archiver) +{ + [McpServerTool, Description("Archive a single failed message by its unique ID. The message will be moved to the archived status.")] + public async Task ArchiveFailedMessage( + [Description("The unique ID of the failed message to archive")] string failedMessageId) + { + await messageSession.SendLocal(m => m.FailedMessageId = failedMessageId); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Archive requested for message '{failedMessageId}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Archive multiple failed messages by their unique IDs. All specified messages will be moved to the archived status.")] + public async Task ArchiveFailedMessages( + [Description("Array of unique message IDs to archive")] string[] messageIds) + { + if (messageIds.Any(string.IsNullOrEmpty)) + { + return JsonSerializer.Serialize(new { Error = "All message IDs must be non-empty strings." }, McpJsonOptions.Default); + } + + foreach (var id in messageIds) + { + await messageSession.SendLocal(m => m.FailedMessageId = id); + } + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Archive requested for {messageIds.Length} messages." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Archive all failed messages in a specific failure group. Failure groups are collections of messages grouped by exception type and stack trace.")] + public async Task ArchiveFailureGroup( + [Description("The ID of the failure group to archive")] string groupId) + { + if (archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) + { + return JsonSerializer.Serialize(new { Status = "InProgress", Message = $"An archive operation is already in progress for group '{groupId}'." }, McpJsonOptions.Default); + } + + await archiver.StartArchiving(groupId, ArchiveType.FailureGroup); + await messageSession.SendLocal(m => m.GroupId = groupId); + + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Archive requested for all messages in failure group '{groupId}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Unarchive a single failed message by its unique ID. The message will be moved back to the unresolved status.")] + public async Task UnarchiveFailedMessage( + [Description("The unique ID of the failed message to unarchive")] string failedMessageId) + { + await messageSession.SendLocal(m => m.FailedMessageIds = [failedMessageId]); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Unarchive requested for message '{failedMessageId}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Unarchive multiple failed messages by their unique IDs. All specified messages will be moved back to the unresolved status.")] + public async Task UnarchiveFailedMessages( + [Description("Array of unique message IDs to unarchive")] string[] messageIds) + { + if (messageIds.Any(string.IsNullOrEmpty)) + { + return JsonSerializer.Serialize(new { Error = "All message IDs must be non-empty strings." }, McpJsonOptions.Default); + } + + await messageSession.SendLocal(m => m.FailedMessageIds = messageIds); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Unarchive requested for {messageIds.Length} messages." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Unarchive all failed messages in a specific failure group. Failure groups are collections of messages grouped by exception type and stack trace.")] + public async Task UnarchiveFailureGroup( + [Description("The ID of the failure group to unarchive")] string groupId) + { + if (archiver.IsOperationInProgressFor(groupId, ArchiveType.FailureGroup)) + { + return JsonSerializer.Serialize(new { Status = "InProgress", Message = $"An archive operation is already in progress for group '{groupId}'." }, McpJsonOptions.Default); + } + + await archiver.StartUnarchiving(groupId, ArchiveType.FailureGroup); + await messageSession.SendLocal(m => m.GroupId = groupId); + + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Unarchive requested for all messages in failure group '{groupId}'." }, McpJsonOptions.Default); + } +} diff --git a/src/ServiceControl/Mcp/FailedMessageTools.cs b/src/ServiceControl/Mcp/FailedMessageTools.cs new file mode 100644 index 0000000000..79c6d47e96 --- /dev/null +++ b/src/ServiceControl/Mcp/FailedMessageTools.cs @@ -0,0 +1,94 @@ +#nullable enable + +namespace ServiceControl.Mcp; + +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; +using MessageFailures.Api; +using ModelContextProtocol.Server; +using Persistence; +using Persistence.Infrastructure; + +[McpServerToolType] +public class FailedMessageTools(IErrorMessageDataStore store) +{ + [McpServerTool, Description("Get a list of failed messages. Supports filtering by status (unresolved, resolved, archived, retryissued), modified date, and queue address. Returns paged results.")] + public async Task GetFailedMessages( + [Description("Filter by status: unresolved, resolved, archived, retryissued")] string? status = null, + [Description("Filter by modified date (ISO 8601 format)")] string? modified = null, + [Description("Filter by queue address")] string? queueAddress = null, + [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, message_type, time_of_failure. Default is time_of_failure")] string sort = "time_of_failure", + [Description("Sort direction: asc or desc. Default is desc")] string direction = "desc") + { + var pagingInfo = new PagingInfo(page, perPage); + var sortInfo = new SortInfo(sort, direction); + + var results = await store.ErrorGet(status, modified, queueAddress, pagingInfo, sortInfo); + + return JsonSerializer.Serialize(new + { + results.QueryStats.TotalCount, + results.Results + }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Get details of a specific failed message by its unique ID.")] + public async Task GetFailedMessageById( + [Description("The unique ID of the failed message")] string failedMessageId) + { + var result = await store.ErrorBy(failedMessageId); + + if (result == null) + { + return JsonSerializer.Serialize(new { Error = $"Failed message '{failedMessageId}' not found." }, McpJsonOptions.Default); + } + + return JsonSerializer.Serialize(result, McpJsonOptions.Default); + } + + [McpServerTool, Description("Get the last processing attempt for a specific failed message.")] + public async Task GetFailedMessageLastAttempt( + [Description("The unique ID of the failed message")] string failedMessageId) + { + var result = await store.ErrorLastBy(failedMessageId); + + if (result == null) + { + return JsonSerializer.Serialize(new { Error = $"Failed message '{failedMessageId}' not found." }, McpJsonOptions.Default); + } + + return JsonSerializer.Serialize(result, McpJsonOptions.Default); + } + + [McpServerTool, Description("Get a summary of error counts grouped by status (unresolved, archived, resolved, retryissued).")] + public async Task GetErrorsSummary() + { + var result = await store.ErrorsSummary(); + return JsonSerializer.Serialize(result, McpJsonOptions.Default); + } + + [McpServerTool, Description("Get failed messages for a specific endpoint.")] + public async Task GetFailedMessagesByEndpoint( + [Description("The name of the endpoint")] string endpointName, + [Description("Filter by status: unresolved, resolved, archived, retryissued")] string? status = null, + [Description("Filter by modified date (ISO 8601 format)")] string? modified = null, + [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, message_type, time_of_failure. Default is time_of_failure")] string sort = "time_of_failure", + [Description("Sort direction: asc or desc. Default is desc")] string direction = "desc") + { + var pagingInfo = new PagingInfo(page, perPage); + var sortInfo = new SortInfo(sort, direction); + + var results = await store.ErrorsByEndpointName(status, endpointName, modified, pagingInfo, sortInfo); + + return JsonSerializer.Serialize(new + { + results.QueryStats.TotalCount, + results.Results + }, McpJsonOptions.Default); + } +} diff --git a/src/ServiceControl/Mcp/FailureGroupTools.cs b/src/ServiceControl/Mcp/FailureGroupTools.cs new file mode 100644 index 0000000000..ec311f4ff8 --- /dev/null +++ b/src/ServiceControl/Mcp/FailureGroupTools.cs @@ -0,0 +1,30 @@ +#nullable enable + +namespace ServiceControl.Mcp; + +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Persistence; +using Recoverability; + +[McpServerToolType] +public class FailureGroupTools(GroupFetcher fetcher, IRetryHistoryDataStore retryStore) +{ + [McpServerTool, Description("Get failure groups, which are collections of failed messages grouped by a classifier (default: exception type and stack trace). Each group shows the count of failures, the first and last occurrence, and any retry operation status.")] + public async Task GetFailureGroups( + [Description("The classifier to group by. Default is 'Exception Type and Stack Trace'")] string classifier = "Exception Type and Stack Trace", + [Description("Optional filter for the classifier")] string? classifierFilter = null) + { + var results = await fetcher.GetGroups(classifier, classifierFilter); + return JsonSerializer.Serialize(results, McpJsonOptions.Default); + } + + [McpServerTool, Description("Get the retry history showing past retry operations and their outcomes.")] + public async Task GetRetryHistory() + { + var retryHistory = await retryStore.GetRetryHistory(); + return JsonSerializer.Serialize(retryHistory, McpJsonOptions.Default); + } +} diff --git a/src/ServiceControl/Mcp/McpJsonOptions.cs b/src/ServiceControl/Mcp/McpJsonOptions.cs new file mode 100644 index 0000000000..1e37e52d37 --- /dev/null +++ b/src/ServiceControl/Mcp/McpJsonOptions.cs @@ -0,0 +1,14 @@ +namespace ServiceControl.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 + }; +} diff --git a/src/ServiceControl/Mcp/RetryTools.cs b/src/ServiceControl/Mcp/RetryTools.cs new file mode 100644 index 0000000000..7d41f9d2f2 --- /dev/null +++ b/src/ServiceControl/Mcp/RetryTools.cs @@ -0,0 +1,84 @@ +namespace ServiceControl.Mcp; + +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using MessageFailures; +using MessageFailures.InternalMessages; +using ModelContextProtocol.Server; +using NServiceBus; +using Recoverability; +using Persistence; + +[McpServerToolType] +public class RetryTools(IMessageSession messageSession, RetryingManager retryingManager) +{ + [McpServerTool, Description("Retry a single failed message by its unique ID. The message will be sent back to its original queue for reprocessing.")] + public async Task RetryFailedMessage( + [Description("The unique ID of the failed message to retry")] string failedMessageId) + { + await messageSession.SendLocal(m => m.FailedMessageId = failedMessageId); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Retry requested for message '{failedMessageId}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Retry multiple failed messages by their unique IDs. All specified messages will be sent back to their original queues for reprocessing.")] + public async Task RetryFailedMessages( + [Description("Array of unique message IDs to retry")] string[] messageIds) + { + if (messageIds.Any(string.IsNullOrEmpty)) + { + return JsonSerializer.Serialize(new { Error = "All message IDs must be non-empty strings." }, McpJsonOptions.Default); + } + + await messageSession.SendLocal(m => m.MessageUniqueIds = messageIds); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Retry requested for {messageIds.Length} messages." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Retry all failed messages from a specific queue address.")] + public async Task RetryFailedMessagesByQueue( + [Description("The queue address to retry all failed messages from")] string queueAddress) + { + await messageSession.SendLocal(m => + { + m.QueueAddress = queueAddress; + m.Status = FailedMessageStatus.Unresolved; + }); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Retry requested for all failed messages in queue '{queueAddress}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Retry all failed messages across all queues. Use with caution as this affects all unresolved failed messages.")] + public async Task RetryAllFailedMessages() + { + await messageSession.SendLocal(new RequestRetryAll()); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = "Retry requested for all failed messages." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Retry all failed messages for a specific endpoint.")] + public async Task RetryAllFailedMessagesByEndpoint( + [Description("The name of the endpoint to retry all failed messages for")] string endpointName) + { + await messageSession.SendLocal(new RequestRetryAll { Endpoint = endpointName }); + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Retry requested for all failed messages in endpoint '{endpointName}'." }, McpJsonOptions.Default); + } + + [McpServerTool, Description("Retry all failed messages in a specific failure group. Failure groups are collections of messages grouped by exception type and stack trace.")] + public async Task RetryFailureGroup( + [Description("The ID of the failure group to retry")] string groupId) + { + if (retryingManager.IsOperationInProgressFor(groupId, RetryType.FailureGroup)) + { + return JsonSerializer.Serialize(new { Status = "InProgress", Message = $"A retry operation is already in progress for group '{groupId}'." }, McpJsonOptions.Default); + } + + var started = System.DateTime.UtcNow; + await retryingManager.Wait(groupId, RetryType.FailureGroup, started); + await messageSession.SendLocal(new RetryAllInGroup + { + GroupId = groupId, + Started = started + }); + + return JsonSerializer.Serialize(new { Status = "Accepted", Message = $"Retry requested for all messages in failure group '{groupId}'." }, McpJsonOptions.Default); + } +} diff --git a/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs b/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs index 2e317cba54..ae852de26e 100644 --- a/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs +++ b/src/ServiceControl/MessageFailures/Handlers/ArchiveMessageHandler.cs @@ -21,7 +21,7 @@ public async Task Handle(ArchiveMessage message, IMessageHandlerContext context) var failedMessage = await dataStore.ErrorBy(failedMessageId); - if (failedMessage.Status != FailedMessageStatus.Archived) + if (failedMessage is not null && failedMessage.Status != FailedMessageStatus.Archived) { await domainEvents.Raise(new FailedMessageArchived { diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index d931751d34..39aea072bc 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -39,6 +39,7 @@ + diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index 685bc7dc16..4de53c6406 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -3,6 +3,8 @@ namespace ServiceControl; using Infrastructure.SignalR; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using ModelContextProtocol.AspNetCore; using ServiceControl.Hosting.ForwardedHeaders; using ServiceControl.Hosting.Https; using ServiceControl.Infrastructure; @@ -19,5 +21,6 @@ public static void UseServiceControl(this WebApplication app, ForwardedHeadersSe app.MapHub("/api/messagestream"); app.UseCors(); app.MapControllers(); + app.MapMcp(); } } \ No newline at end of file From d00a4c474c42f91c936ca3d5d57739802432446a Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 12:11:14 +0200 Subject: [PATCH 02/31] Add feature flag check for MCP --- .../TestSupport/ServiceControlComponentRunner.cs | 4 ++-- .../Hosting/Commands/ImportFailedErrorsCommand.cs | 2 +- src/ServiceControl/Hosting/Commands/RunCommand.cs | 4 ++-- .../WebApi/HostApplicationBuilderExtensions.cs | 7 +++---- src/ServiceControl/WebApplicationExtensions.cs | 11 +++++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 657a84244d..0d23f4febc 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -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); @@ -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); await host.StartAsync(); DomainEvents = host.Services.GetRequiredService(); // Bring this back and look into the base address of the client diff --git a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs index 105f756daf..932e301047 100644 --- a/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs +++ b/src/ServiceControl/Hosting/Commands/ImportFailedErrorsCommand.cs @@ -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(); diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index ebc08958cf..e3d391ca12 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -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); if (settings.EnableIntegratedServicePulse) { app.UseServicePulse(settings.ServicePulseSettings); diff --git a/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs b/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs index 173ce94b70..17dc44d5d3 100644 --- a/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs @@ -10,11 +10,10 @@ using Particular.LicensingComponent.WebApi; using Particular.ServiceControl; using ServiceBus.Management.Infrastructure.Settings; - using ServiceControl.Configuration; static class HostApplicationBuilderExtensions { - public static void AddServiceControlApi(this IHostApplicationBuilder builder, CorsSettings corsSettings) + public static void AddServiceControlApi(this IHostApplicationBuilder builder, Settings settings) { // This registers concrete classes that implement IApi. Currently it is hard to find out to what // component those APIs should belong to so we leave it here for now. @@ -22,7 +21,7 @@ public static void AddServiceControlApi(this IHostApplicationBuilder builder, Co builder.AddServiceControlApis(); - if (SettingsReader.Read(Settings.SettingsRootNamespace, "EnableMcpServer", false)) + if (settings.EnableMcpServer) { builder.Services .AddMcpServer() @@ -30,7 +29,7 @@ public static void AddServiceControlApi(this IHostApplicationBuilder builder, Co .WithToolsFromAssembly(); } - builder.Services.AddCors(options => options.AddDefaultPolicy(Cors.GetDefaultPolicy(corsSettings))); + 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(); diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index 4de53c6406..c912c9756b 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -3,15 +3,14 @@ namespace ServiceControl; using Infrastructure.SignalR; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; -using ModelContextProtocol.AspNetCore; +using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Hosting.ForwardedHeaders; using ServiceControl.Hosting.Https; using ServiceControl.Infrastructure; public static class WebApplicationExtensions { - public static void UseServiceControl(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings) + public static void UseServiceControl(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings, Settings settings) { app.UseServiceControlForwardedHeaders(forwardedHeadersSettings); app.UseServiceControlHttps(httpsSettings); @@ -21,6 +20,10 @@ public static void UseServiceControl(this WebApplication app, ForwardedHeadersSe app.MapHub("/api/messagestream"); app.UseCors(); app.MapControllers(); - app.MapMcp(); + + if (settings.EnableMcpServer) + { + app.MapMcp(); + } } } \ No newline at end of file From 68fb5cc3ca44f5418fbd77bd84d5a453ceb50a70 Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 12:37:25 +0200 Subject: [PATCH 03/31] Update to v1.1.0 of ModelContextProtocol.AspNetCore --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d78bd8f04b..ce84976ca5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -83,7 +83,7 @@ - + From 44b742f1166d43d287e9256ca4961ae9033d8f1f Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 12:38:39 +0200 Subject: [PATCH 04/31] Turn MCP off by default --- src/ServiceControl/App.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl/App.config b/src/ServiceControl/App.config index 817bd95e11..698755c9a7 100644 --- a/src/ServiceControl/App.config +++ b/src/ServiceControl/App.config @@ -5,7 +5,7 @@ These settings are only here so that we can debug ServiceControl while developin --> - + From 7fb5d1ca83963459b8c5f52b266abefd664cd371 Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 12:40:15 +0200 Subject: [PATCH 05/31] Put packages in alphabetical order --- src/ServiceControl/ServiceControl.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 39aea072bc..2475998650 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -33,13 +33,13 @@ + - From 59dd484daa7f6452d3a0a7539af724dd355f21f8 Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 14:07:21 +0200 Subject: [PATCH 06/31] Update approvals --- .../APIApprovals.PlatformSampleSettings.approved.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt index 6873e229b3..5de2540e03 100644 --- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt +++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.PlatformSampleSettings.approved.txt @@ -37,6 +37,7 @@ }, "NotificationsFilter": null, "AllowMessageEditing": false, + "EnableMcpServer": false, "EnableIntegratedServicePulse": false, "ServicePulseSettings": null, "MessageFilter": null, From bec8c4efcc1c93fd30223cdfe883bb6a402d2220 Mon Sep 17 00:00:00 2001 From: WilliamBZA Date: Mon, 9 Mar 2026 15:30:59 +0200 Subject: [PATCH 07/31] Don't pass the full settings object in --- .../TestSupport/ServiceControlComponentRunner.cs | 2 +- src/ServiceControl/Hosting/Commands/RunCommand.cs | 2 +- src/ServiceControl/WebApplicationExtensions.cs | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index 0d23f4febc..b6b3b8048a 100644 --- a/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -135,7 +135,7 @@ async Task InitializeServiceControl(ScenarioContext context) host.UseTestRemoteIp(); host.UseServiceControlAuthentication(settings.OpenIdConnectSettings.Enabled); - host.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings); + host.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer); await host.StartAsync(); DomainEvents = host.Services.GetRequiredService(); // Bring this back and look into the base address of the client diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index e3d391ca12..9778db2cc0 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -30,7 +30,7 @@ public override async Task Execute(HostArguments args, Settings settings) hostBuilder.AddServiceControlApi(settings); var app = hostBuilder.Build(); - app.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings); + app.UseServiceControl(settings.ForwardedHeadersSettings, settings.HttpsSettings, settings.EnableMcpServer); if (settings.EnableIntegratedServicePulse) { app.UseServicePulse(settings.ServicePulseSettings); diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index c912c9756b..ac015a5c5b 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -3,14 +3,13 @@ namespace ServiceControl; using Infrastructure.SignalR; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; -using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Hosting.ForwardedHeaders; using ServiceControl.Hosting.Https; using ServiceControl.Infrastructure; public static class WebApplicationExtensions { - public static void UseServiceControl(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings, Settings settings) + public static void UseServiceControl(this WebApplication app, ForwardedHeadersSettings forwardedHeadersSettings, HttpsSettings httpsSettings, bool enableMcpServer) { app.UseServiceControlForwardedHeaders(forwardedHeadersSettings); app.UseServiceControlHttps(httpsSettings); @@ -21,7 +20,7 @@ public static void UseServiceControl(this WebApplication app, ForwardedHeadersSe app.UseCors(); app.MapControllers(); - if (settings.EnableMcpServer) + if (enableMcpServer) { app.MapMcp(); } From 484a80fffdd3045f7319d4459835b300cb4850c5 Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Tue, 10 Mar 2026 13:04:06 +0800 Subject: [PATCH 08/31] Document Integrated ServicePulse in README (#5344) * Document Integrated ServicePulse in README Added section about Integrated ServicePulse and its hosting capabilities. * Apply suggestion from @PhilBastian Co-authored-by: Phil Bastian <155411597+PhilBastian@users.noreply.github.com> * Fix typo --------- Co-authored-by: Phil Bastian <155411597+PhilBastian@users.noreply.github.com> --- README.md | 6 ++++++ src/ServiceControl/Container-README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7261e84be4..0668ad072b 100644 --- a/README.md +++ b/README.md @@ -102,3 +102,9 @@ Steps: -ErrorRetentionPeriod 10:00:00:00 ` -Acknowledgements RabbitMQBrokerVersion310 ``` + +## Integrated ServicePulse + +Since version 6.13, ServiceControl ships with a copy of ServicePulse and [can host it from an Error instance](https://docs.particular.net/servicecontrol/servicecontrol-instances/integrated-servicepulse). + +ServiceControl Error instances have a reference to the Particular.ServicePulse.Core package; this contains the ServicePulse assets, along with the code required to serve them out of an ASP.NET web host. diff --git a/src/ServiceControl/Container-README.md b/src/ServiceControl/Container-README.md index 011e8a7f00..806df8c49c 100644 --- a/src/ServiceControl/Container-README.md +++ b/src/ServiceControl/Container-README.md @@ -20,7 +20,7 @@ docker run -d --name servicecontrol -p 33333:33333 \ -e CONNECTIONSTRING="host=rabbitmq" \ -e RAVENDB_CONNECTIONSTRING="http://servicecontrol-db:8080" \ -e REMOTEINSTANCES='[{"api_uri":"http://audit:44444/api"}]' \ - -e ENABLEDINTEGRATEDSERVICEPULSE="true" \ + -e ENABLEINTEGRATEDSERVICEPULSE="true" \ particular/servicecontrol:latest --setup-and-run ``` From a76aea4bc3a7791f57db9ddf59060aca20cce7eb Mon Sep 17 00:00:00 2001 From: "internalautomation[bot]" <85681268+internalautomation[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:24:20 -0500 Subject: [PATCH 09/31] GitHubSync update - master (#5338) Co-authored-by: internalautomation[bot] <85681268+internalautomation[bot]@users.noreply.github.com> --- nuget.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index ff985a1e0a..d72d1d3d28 100644 --- a/nuget.config +++ b/nuget.config @@ -15,4 +15,4 @@ - \ No newline at end of file + From d3595df428edab5d450fa6e1acdd514fdbb68780 Mon Sep 17 00:00:00 2001 From: "internalautomation[bot]" <85681268+internalautomation[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:08:46 -0500 Subject: [PATCH 10/31] GitHubSync update - master (#5375) Co-authored-by: internalautomation[bot] <85681268+internalautomation[bot]@users.noreply.github.com> --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4b7369cc5e..cb3436b7a2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ low all - 2.1.3 + 2.1.4 0024000004800000940000000602000000240000525341310004000001000100dde965e6172e019ac82c2639ffe494dd2e7dd16347c34762a05732b492e110f2e4e2e1b5ef2d85c848ccfb671ee20a47c8d1376276708dc30a90ff1121b647ba3b7259a6bc383b2034938ef0e275b58b920375ac605076178123693c6c4f1331661a62eba28c249386855637780e3ff5f23a6d854700eaa6803ef48907513b92 00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5 From a3c757bedf8a43152e4d34e0dae31ec0d9eb9523 Mon Sep 17 00:00:00 2001 From: Tamara Rivera Date: Thu, 12 Mar 2026 17:55:31 -0700 Subject: [PATCH 11/31] Remove an unreleased package (#5379) --- src/Directory.Packages.props | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ce84976ca5..50d8e75c50 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -81,7 +81,6 @@ - @@ -95,4 +94,4 @@ - \ No newline at end of file + From e0404aa2c9ab934989d7dfbb632d2838ca85a518 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:13:08 +0000 Subject: [PATCH 12/31] Update dependency NUnit.Analyzers to 4.12.0 (#5349) * Update dependency NUnit.Analyzers to 4.12.0 * Fix assert condition * In-memory body storage should set ETag Even if it is not used anywhere --------- Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> Co-authored-by: Tamara Rivera Co-authored-by: Mike Minutillo --- src/Directory.Packages.props | 2 +- .../InMemoryAttachmentsBodyStorage.cs | 3 ++- src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 50d8e75c50..e3584d54a1 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -54,7 +54,7 @@ - + diff --git a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs index b46d3b1fac..d2193c57fe 100644 --- a/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs +++ b/src/ServiceControl.Audit.Persistence.InMemory/InMemoryAttachmentsBodyStorage.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Audit.Persistence.InMemory { + using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -60,7 +61,7 @@ public async Task TryFetch(string bodyId, CancellationToken cancel Stream = new MemoryStream(messageBody.Content), ContentType = messageBody.ContentType, BodySize = messageBody.BodySize, - Etag = string.Empty + Etag = Guid.NewGuid().ToString() }); } diff --git a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs index 9dcffaee85..52f360ae46 100644 --- a/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs +++ b/src/ServiceControl.Audit.Persistence.Tests/AuditTests.cs @@ -101,7 +101,7 @@ public async Task Can_roundtrip_message_body() Assert.That(retrievedMessage.Found, Is.True); Assert.That(retrievedMessage.HasContent, Is.True); Assert.That(retrievedMessage.ContentLength, Is.EqualTo(body.Length)); - Assert.That(retrievedMessage.ETag, Is.Not.Null.Or.Empty); + Assert.That(retrievedMessage.ETag, Is.Not.Null.And.Not.Empty); Assert.That(retrievedMessage.StreamContent, Is.Not.Null); Assert.That(retrievedMessage.ContentType, Is.EqualTo(expectedContentType)); }); From 076dda85422d77b70e26d5cef04a040e4e9a1eec Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:14:04 +0100 Subject: [PATCH 13/31] Update dependency System.ServiceProcess.ServiceController to 10.0.5 (#5371) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e3584d54a1..ea039f0a3b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -80,7 +80,7 @@ - + From 3a2c0e4464e394acbca315c2f9da82853f968790 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:14:26 +0100 Subject: [PATCH 14/31] Update dependency Microsoft.Extensions.TimeProvider.Testing to 10.4.0 (#5373) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ea039f0a3b..61e50c2c74 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -27,7 +27,7 @@ - + From 90129c99311701499d518be9655cd3b13fc3765e Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:14:49 +0100 Subject: [PATCH 15/31] Update dependency System.IO.Hashing to 10.0.5 (#5368) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 61e50c2c74..ae6668259d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -87,7 +87,7 @@ - + From 96939269ef0f8bedd22f2be6d98eace69e98e029 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:15:13 +0100 Subject: [PATCH 16/31] Update dependency System.Configuration.ConfigurationManager to 10.0.5 (#5365) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ae6668259d..7fe5910e9a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -72,7 +72,7 @@ - + From 3c907f2f227e341c62b9e90682db6f758f22a026 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:16:17 +0100 Subject: [PATCH 17/31] Update docker/setup-buildx-action action to v4 (#5345) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/build-containers.yml | 2 +- .github/workflows/build-db-container.yml | 2 +- .github/workflows/push-container-images.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 6f5fec5907..03a7e9ef7f 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -47,7 +47,7 @@ jobs: with: version: ${{ env.MinVerVersion }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.12.0 + uses: docker/setup-buildx-action@v4.0.0 - name: Log in to GitHub container registry uses: docker/login-action@v3.7.0 with: diff --git a/.github/workflows/build-db-container.yml b/.github/workflows/build-db-container.yml index 6cae00fabc..21da8c658f 100644 --- a/.github/workflows/build-db-container.yml +++ b/.github/workflows/build-db-container.yml @@ -31,7 +31,7 @@ jobs: with: version: ${{ env.MinVerVersion }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.12.0 + uses: docker/setup-buildx-action@v4.0.0 - name: Log in to GitHub container registry uses: docker/login-action@v3.7.0 with: diff --git a/.github/workflows/push-container-images.yml b/.github/workflows/push-container-images.yml index f556b7da43..d5fc93ee62 100644 --- a/.github/workflows/push-container-images.yml +++ b/.github/workflows/push-container-images.yml @@ -33,7 +33,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.12.0 + uses: docker/setup-buildx-action@v4.0.0 - name: Publish to Docker Hub run: | $containers = @('servicecontrol', 'servicecontrol-audit', 'servicecontrol-monitoring', 'servicecontrol-ravendb') From f0dc5a578576a5bb479ea65e3e8e28ef91936f9a Mon Sep 17 00:00:00 2001 From: Tamara Rivera Date: Fri, 13 Mar 2026 16:53:07 -0700 Subject: [PATCH 18/31] Add new groups to Renovate configuration (#5381) * Add .NET group to Renovate configuration Added a new group for .NET related dependencies in Renovate configuration. * Match with regular expressions Updated package name matching patterns for .NET * Add IdentityModel group to Renovate configuration Added a new group for IdentityModel packages in Renovate configuration. * Tweaks * Add OpenTelemetry group --------- Co-authored-by: Brandon Ording --- .github/renovate.json5 | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6b5b11befb..8fb6561a6c 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,5 +1,37 @@ { + $schema: "https://docs.renovatebot.com/renovate-schema.json", packageRules: [ + { + description:"Packages that ship as part of .NET", + matchDatasources: ["nuget"], + matchPackageNames: [ + "/^Microsoft.AspNetCore./", + "/^Microsoft.Extensions./", + "/^System./", + "!/^System.IdentityModel./", + "!/^System.Management.Automation/", + "!/^System.Reactive/" + ], + groupName: ".NET Packages", + groupSlug: "dotnet-packages" + }, + { + description:"IdentityModel packages that release separately from .NET", + matchDatasources: ["nuget"], + matchPackageNames: [ + "/^Microsoft.IdentityModel./", + "/^System.IdentityModel./" + ], + groupName: "IdentityModel", + groupSlug: "identity-model" + }, + { + description:"OpenTelemetry packages", + matchDatasources: ["nuget"], + matchPackageNames: ["/^OpenTelemetry./"], + groupName: "OpenTelemetry", + groupSlug: "open-telemetry" + }, { description: "Keep ServiceControl.Management.PowerShell on 8.x", matchFileNames: ["**/ServiceControl.Management.PowerShell.csproj"], From 6e5b1cf829e7a829e5f1a54d35a22101f3220a8d Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:13:22 +0100 Subject: [PATCH 19/31] Update dependency NLog.Extensions.Logging to 6.1.2 (#5337) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7fe5910e9a..906fe04546 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -33,7 +33,7 @@ - + From c98bdf39844e34f5c41b510ebfeff78dfa12a07b Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:37:27 +0100 Subject: [PATCH 20/31] Update .NET Packages to 10.0.5 (#5382) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 906fe04546..2fdc07df6e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -16,17 +16,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -73,13 +73,13 @@ - - + + - + - + From 93d6c8c7a77b87961bb01a1269142b69e3b74a8f Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:37:52 +0100 Subject: [PATCH 21/31] Update docker/build-push-action action to v7 (#5346) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/build-containers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 03a7e9ef7f..fb42dd4697 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -58,7 +58,7 @@ jobs: id: date run: echo "date=$(date '+%FT%TZ')" >> $GITHUB_OUTPUT - name: Build and push image to GitHub container registry - uses: docker/build-push-action@v6.19.2 + uses: docker/build-push-action@v7.0.0 with: context: . push: true From 97a1092bfca767d51e0d5bf45043c2e8cfe201cd Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:38:07 +0100 Subject: [PATCH 22/31] Update dependency Polly.Core to 8.6.6 (#5341) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 2fdc07df6e..d542e53d1f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -64,7 +64,7 @@ - + From 48eb4a644878daaf058eaec6e6c2c06060b10e83 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:28:53 -0400 Subject: [PATCH 23/31] Update dependency System.Management.Automation to 7.4.14 (#5377) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d542e53d1f..621b99e9d3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -77,7 +77,7 @@ - + From 12800b73f3d9148a8ee4e1f1dafa1cf510d8624d Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 16 Mar 2026 11:05:27 +0100 Subject: [PATCH 24/31] Upgrade to deprecation changes in NServiceBus.RabbitMQ 11.1.0 (#5383) Co-authored-by: Daniel Marbach --- src/Directory.Packages.props | 2 +- .../RabbitMQTransportExtensions.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 621b99e9d3..ca5c2df554 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -44,7 +44,7 @@ - + diff --git a/src/ServiceControl.Transports.RabbitMQ/RabbitMQTransportExtensions.cs b/src/ServiceControl.Transports.RabbitMQ/RabbitMQTransportExtensions.cs index ebf285839c..b4dc699228 100644 --- a/src/ServiceControl.Transports.RabbitMQ/RabbitMQTransportExtensions.cs +++ b/src/ServiceControl.Transports.RabbitMQ/RabbitMQTransportExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; +using global::RabbitMQ.Client; using NServiceBus; static class RabbitMQTransportExtensions @@ -40,7 +41,7 @@ public static void ApplySettingsFromConnectionString(this RabbitMQTransport tran if (dictionary.TryGetValue("UseExternalAuthMechanism", out var useExternalAuthMechanismString)) { _ = bool.TryParse(useExternalAuthMechanismString, out var useExternalAuthMechanism); - transport.UseExternalAuthMechanism = useExternalAuthMechanism; + transport.AuthMechanisms = [new ExternalMechanismFactory()]; } } } From a3dda547c6431578984821f227420038151bc551 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:32:12 +0000 Subject: [PATCH 25/31] Update dependency Azure.Identity to 1.19.0 (#5332) * Update dependency Azure.Identity to 1.19.0 * Switch to `ManagedIdentityId` for token credential authentication --------- Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> Co-authored-by: Daniel Marbach --- src/Directory.Packages.props | 2 +- .../TokenCredentialAuthentication.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ca5c2df554..8c73b37359 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/src/ServiceControl.Transports.ASBS/TokenCredentialAuthentication.cs b/src/ServiceControl.Transports.ASBS/TokenCredentialAuthentication.cs index a11f74463f..d00e277891 100644 --- a/src/ServiceControl.Transports.ASBS/TokenCredentialAuthentication.cs +++ b/src/ServiceControl.Transports.ASBS/TokenCredentialAuthentication.cs @@ -1,4 +1,5 @@ -namespace ServiceControl.Transports.ASBS +#nullable enable +namespace ServiceControl.Transports.ASBS { using Azure.Core; using Azure.Identity; @@ -13,21 +14,20 @@ public TokenCredentialAuthentication(string fullyQualifiedNamespace) Credential = new DefaultAzureCredential(); } - public TokenCredentialAuthentication(string fullyQualifiedNamespace, string clientId) + public TokenCredentialAuthentication(string fullyQualifiedNamespace, string? clientId) { FullyQualifiedNamespace = fullyQualifiedNamespace; ClientId = clientId; - Credential = new ManagedIdentityCredential(clientId); + Credential = new ManagedIdentityCredential(clientId is not null ? ManagedIdentityId.FromUserAssignedClientId(clientId) : ManagedIdentityId.SystemAssigned); } public string FullyQualifiedNamespace { get; } public TokenCredential Credential { get; } - public string ClientId { get; } + public string? ClientId { get; } - public override ServiceBusAdministrationClient BuildManagementClient() - => new ServiceBusAdministrationClient(FullyQualifiedNamespace, Credential); + public override ServiceBusAdministrationClient BuildManagementClient() => new(FullyQualifiedNamespace, Credential); public override AzureServiceBusTransport CreateTransportDefinition(ConnectionSettings connectionSettings, TopicTopology topology) { From 7f1dcc4cb375e82df234088e8a4c6182711e8ace Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:52:12 +0000 Subject: [PATCH 26/31] Update azure/login action to v3 (#5385) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/container-integration-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a8b91de03..020d1ade49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: if: matrix.os-name == 'Windows' run: Import-Module ./deploy/PowerShellModules/Particular.ServiceControl.Management - name: Azure login - uses: azure/login@v2.3.0 + uses: azure/login@v3.0.0 if: matrix.test-category == 'AzureServiceBus' || matrix.test-category == 'AzureStorageQueues' || matrix.test-category == 'RabbitMQ' || matrix.test-category == 'PostgreSQL' with: creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} diff --git a/.github/workflows/container-integration-test.yml b/.github/workflows/container-integration-test.yml index 1b59cd927b..c0a9fd1664 100644 --- a/.github/workflows/container-integration-test.yml +++ b/.github/workflows/container-integration-test.yml @@ -72,7 +72,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Azure Login if: ${{ matrix.name == 'asb' || matrix.name == 'asq' }} - uses: azure/login@v2.3.0 + uses: azure/login@v3.0.0 with: creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} - name: Setup Azure Service Bus From 2f7f0de9fa981adaf90b413d9785422f528f39d9 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:46:27 +0000 Subject: [PATCH 27/31] Update Particular/setup-azureservicebus-action action to v2.1.0 (#5389) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/container-integration-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 020d1ade49..89d17f3c27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: registry-username: ${{ secrets.DOCKERHUB_USERNAME }} registry-password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup Azure Service Bus - uses: Particular/setup-azureservicebus-action@v2.0.0 + uses: Particular/setup-azureservicebus-action@v2.1.0 if: matrix.test-category == 'AzureServiceBus' with: connection-string-name: ServiceControl_TransportTests_ASBS_ConnectionString diff --git a/.github/workflows/container-integration-test.yml b/.github/workflows/container-integration-test.yml index c0a9fd1664..413dba6e8c 100644 --- a/.github/workflows/container-integration-test.yml +++ b/.github/workflows/container-integration-test.yml @@ -77,7 +77,7 @@ jobs: creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} - name: Setup Azure Service Bus if: ${{ matrix.name == 'asb' }} - uses: Particular/setup-azureservicebus-action@v2.0.0 + uses: Particular/setup-azureservicebus-action@v2.1.0 with: connection-string-name: CONNECTIONSTRING azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} From 429bb27aa3f77bdc0351703f2490af28691f7ee2 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:25:07 +1000 Subject: [PATCH 28/31] Update dependency NServiceBus.Transport.AzureServiceBus to 6.2.0 (#5388) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8c73b37359..9d874478da 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -47,7 +47,7 @@ - + From 596f9864367e297fbf6ab4f3b4a22436b9f036d1 Mon Sep 17 00:00:00 2001 From: williambza Date: Mon, 9 Mar 2026 11:35:16 +0200 Subject: [PATCH 29/31] Add failed message MCP server --- src/ServiceControl/ServiceControl.csproj | 1 + src/ServiceControl/WebApplicationExtensions.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 2475998650..629b12dea0 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -40,6 +40,7 @@ + diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index ac015a5c5b..5907ff8134 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -3,6 +3,8 @@ namespace ServiceControl; using Infrastructure.SignalR; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using ModelContextProtocol.AspNetCore; using ServiceControl.Hosting.ForwardedHeaders; using ServiceControl.Hosting.Https; using ServiceControl.Infrastructure; From ff80665e380c9625650dafa395f1d5d4be80c0e3 Mon Sep 17 00:00:00 2001 From: williambza Date: Fri, 20 Mar 2026 12:20:01 +0200 Subject: [PATCH 30/31] Use /mcp as the route --- src/ServiceControl/WebApplicationExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index 5907ff8134..4d3be18f2c 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -24,7 +24,7 @@ public static void UseServiceControl(this WebApplication app, ForwardedHeadersSe if (enableMcpServer) { - app.MapMcp(); + app.MapMcp("/mcp"); } } } \ No newline at end of file From 4555051b0ca4ca3912454d9127847f1c153b683f Mon Sep 17 00:00:00 2001 From: williambza Date: Fri, 20 Mar 2026 12:33:48 +0200 Subject: [PATCH 31/31] Add MCP for audit --- .../ServiceControlComponentRunner.cs | 4 +- src/ServiceControl.Audit/App.config | 2 + .../Hosting/Commands/RunCommand.cs | 4 +- .../Infrastructure/Settings/Settings.cs | 3 + .../HostApplicationBuilderExtensions.cs | 13 +- .../Mcp/AuditMessageTools.cs | 147 ++++++++++++++++++ src/ServiceControl.Audit/Mcp/EndpointTools.cs | 40 +++++ .../Mcp/McpJsonOptions.cs | 16 ++ .../ServiceControl.Audit.csproj | 1 + .../WebApplicationExtensions.cs | 7 +- 10 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 src/ServiceControl.Audit/Mcp/AuditMessageTools.cs create mode 100644 src/ServiceControl.Audit/Mcp/EndpointTools.cs create mode 100644 src/ServiceControl.Audit/Mcp/McpJsonOptions.cs diff --git a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs index efcd99c0f6..c197a2e54a 100644 --- a/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs +++ b/src/ServiceControl.Audit.AcceptanceTests/TestSupport/ServiceControlComponentRunner.cs @@ -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); @@ -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(); diff --git a/src/ServiceControl.Audit/App.config b/src/ServiceControl.Audit/App.config index a3f5781c51..8adfc4075d 100644 --- a/src/ServiceControl.Audit/App.config +++ b/src/ServiceControl.Audit/App.config @@ -8,6 +8,8 @@ These settings are only here so that we can debug ServiceControl while developin + + diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs index 22e2fff776..7ddfcf46cd 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs @@ -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); diff --git a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs index 3203bd349e..22ec971d12 100644 --- a/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs +++ b/src/ServiceControl.Audit/Infrastructure/Settings/Settings.cs @@ -54,6 +54,7 @@ public Settings(string transportType = null, string persisterType = null, Loggin ServiceControlQueueAddress = SettingsReader.Read(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); @@ -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 diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs index 638041d4b1..f650640314 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/HostApplicationBuilderExtensions.cs @@ -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() + .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(); diff --git a/src/ServiceControl.Audit/Mcp/AuditMessageTools.cs b/src/ServiceControl.Audit/Mcp/AuditMessageTools.cs new file mode 100644 index 0000000000..b4fddeaa51 --- /dev/null +++ b/src/ServiceControl.Audit/Mcp/AuditMessageTools.cs @@ -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] +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.")] + public async Task 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.")] + public async Task 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 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 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 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); + } +} diff --git a/src/ServiceControl.Audit/Mcp/EndpointTools.cs b/src/ServiceControl.Audit/Mcp/EndpointTools.cs new file mode 100644 index 0000000000..705a88fbb2 --- /dev/null +++ b/src/ServiceControl.Audit/Mcp/EndpointTools.cs @@ -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 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 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); + } +} diff --git a/src/ServiceControl.Audit/Mcp/McpJsonOptions.cs b/src/ServiceControl.Audit/Mcp/McpJsonOptions.cs new file mode 100644 index 0000000000..ff03d91eae --- /dev/null +++ b/src/ServiceControl.Audit/Mcp/McpJsonOptions.cs @@ -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 + }; +} diff --git a/src/ServiceControl.Audit/ServiceControl.Audit.csproj b/src/ServiceControl.Audit/ServiceControl.Audit.csproj index 1752bf81bd..b7394443c1 100644 --- a/src/ServiceControl.Audit/ServiceControl.Audit.csproj +++ b/src/ServiceControl.Audit/ServiceControl.Audit.csproj @@ -26,6 +26,7 @@ + diff --git a/src/ServiceControl.Audit/WebApplicationExtensions.cs b/src/ServiceControl.Audit/WebApplicationExtensions.cs index 76785dd77d..e8edece77f 100644 --- a/src/ServiceControl.Audit/WebApplicationExtensions.cs +++ b/src/ServiceControl.Audit/WebApplicationExtensions.cs @@ -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); @@ -17,5 +17,10 @@ public static void UseServiceControlAudit(this WebApplication app, ForwardedHead app.UseHttpLogging(); app.UseCors(); app.MapControllers(); + + if (enableMcpServer) + { + app.MapMcp("/mcp"); + } } } \ No newline at end of file