From aa2086c04f788704dbbf156969a5bb03b5a4020a Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Sat, 2 May 2026 00:28:14 +1000 Subject: [PATCH 1/3] Support Seq servers without the Scan link --- README.md | 32 ++++++++++----- docs/DEVELOPMENT.md | 12 +++++- src/SeqMcpServer/Mcp/SeqTools.cs | 40 ++++++++++++++++--- src/SeqMcpServer/SeqMcpServer.csproj | 4 +- .../McpToolsIntegrationTests.cs | 40 +++++++++++++++++-- .../SeqMcpServer.Tests.csproj | 2 +- 6 files changed, 108 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 61d86be..0cb8ada 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ dotnet tool uninstall -g SeqMcpServer ### Requirements -- .NET 9.0 Runtime or SDK -- Seq server (local or remote) -- Valid Seq API key +- .NET 10.0 Runtime or SDK +- Seq server (local or remote) +- Valid Seq API key ## Quick Start @@ -143,7 +143,7 @@ Download the latest release for your platform and add to your MCP settings: ### Option 3: Build from Source -Build a single-file executable (requires .NET 9 runtime): +Build a single-file executable (requires .NET 10 runtime): ```bash # Windows @@ -156,15 +156,29 @@ dotnet publish -c Release -r osx-x64 -p:PublishSingleFile=true dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true ``` -The executable will be in `SeqMcpServer/bin/Release/net9.0/{runtime}/publish/` +The executable will be in `SeqMcpServer/bin/Release/net10.0/{runtime}/publish/` ## Configuration The Seq MCP Server uses environment variables for configuration: - `SEQ_SERVER_URL`: URL of your Seq server -- `SEQ_API_KEY`: API key for accessing Seq (required) -- `SEQ_API_KEY_`: Optional workspace-specific API keys (e.g., `SEQ_API_KEY_PRODUCTION`) +- `SEQ_API_KEY`: API key for accessing Seq (required) +- `SEQ_API_KEY_`: Optional workspace-specific API keys (e.g., `SEQ_API_KEY_PRODUCTION`) + +### Seq Compatibility + +`SeqSearch` prefers `Events.EnumerateAsync()`, which uses the Seq `Scan` link when the server advertises it. Older Seq builds such as `2024.3.x` do not expose `Scan` on `api/events/resources`; in that case the server now falls back to `PagedEnumerateAsync()` so searches continue to work instead of failing with: + +```text +System.NotSupportedException: The requested link `Scan` isn't available on entity `Seq.Api.Model.ResourceGroup`. +``` + +If you are debugging compatibility issues: + +- Seq `2025.2.x` and newer expose `Scan` +- Seq `2024.3.x` does not expose `Scan` +- this MCP server supports both paths by falling back automatically ### Workspace Support @@ -182,7 +196,7 @@ export SEQ_API_KEY_STAGING="staging-key" ### Prerequisites -- .NET 9.0 SDK +- .NET 10.0 SDK - Docker (for running Seq locally) ### Running Tests @@ -222,4 +236,4 @@ The MCP server can log its own operations to Seq when a valid `SEQ_SERVER_URL` a ## License -MIT License - see [LICENSE](LICENSE) file for details. \ No newline at end of file +MIT License - see [LICENSE](LICENSE) file for details. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2861072..ee0e548 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -89,6 +89,16 @@ The application uses environment variables for configuration. These can be set v - `SEQ_SERVER_URL`: URL of your Seq server (e.g., `http://localhost:18081`) - `SEQ_API_KEY`: API key for accessing Seq +## Compatibility Notes + +Seq `2024.3.x` does not expose the `Scan` link under `api/events/resources`. If `SeqSearch` is implemented only through `Events.EnumerateAsync()`, searches can fail with: + +```text +System.NotSupportedException: The requested link `Scan` isn't available on entity `Seq.Api.Model.ResourceGroup`. +``` + +The current implementation falls back to `PagedEnumerateAsync()` when `Scan` is unavailable so local development against older Seq containers remains functional. + ### Workspace-Specific Keys (Optional) You can configure different API keys for different workspaces: @@ -140,4 +150,4 @@ For production: - Use strong, unique passwords - Store credentials securely (e.g., Azure Key Vault, environment variables) - Enable proper authentication and authorization -- Use HTTPS for all connections \ No newline at end of file +- Use HTTPS for all connections diff --git a/src/SeqMcpServer/Mcp/SeqTools.cs b/src/SeqMcpServer/Mcp/SeqTools.cs index 2bc6962..0f954e9 100644 --- a/src/SeqMcpServer/Mcp/SeqTools.cs +++ b/src/SeqMcpServer/Mcp/SeqTools.cs @@ -34,14 +34,42 @@ public static async Task> SeqSearch( { var conn = fac.Create(workspace); var events = new List(); - await foreach (var evt in conn.Events.EnumerateAsync( - filter: filter, - count: count, - render: true, - cancellationToken: ct).WithCancellation(ct)) + + try { - events.Add(evt); + await foreach (var evt in conn.Events.EnumerateAsync( + filter: filter, + count: count, + render: true, + cancellationToken: ct).WithCancellation(ct)) + { + events.Add(evt); + } } + catch (NotSupportedException ex) when (ex.Message.Contains("Scan", StringComparison.OrdinalIgnoreCase)) + { + // Older or differently-configured Seq APIs may not expose the Scan link used by EnumerateAsync(). + await foreach (var evt in conn.Events.PagedEnumerateAsync( + unsavedSignal: null, + signal: null, + filter: filter, + count: count, + startAtId: null, + afterId: null, + render: true, + fromDateUtc: null, + toDateUtc: null, + shortCircuitAfter: null, + permalinkId: null, + variables: null, + background: false, + trace: false, + cancellationToken: ct).WithCancellation(ct)) + { + events.Add(evt); + } + } + return events; } catch (OperationCanceledException) diff --git a/src/SeqMcpServer/SeqMcpServer.csproj b/src/SeqMcpServer/SeqMcpServer.csproj index 4d049d2..adc550f 100644 --- a/src/SeqMcpServer/SeqMcpServer.csproj +++ b/src/SeqMcpServer/SeqMcpServer.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable @@ -37,7 +37,7 @@ - + diff --git a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs index 613f25e..13875fc 100644 --- a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs +++ b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs @@ -103,7 +103,7 @@ public async Task InitializeAsync() // Create MCP client transport // Build path relative to the test assembly location var testAssemblyLocation = Path.GetDirectoryName(typeof(McpToolsIntegrationTests).Assembly.Location)!; - var serverDllPath = Path.GetFullPath(Path.Combine(testAssemblyLocation, "../../../../../src/SeqMcpServer/bin/Debug/net9.0/SeqMcpServer.dll")); + var serverDllPath = Path.GetFullPath(Path.Combine(testAssemblyLocation, "../../../../../src/SeqMcpServer/bin/Debug/net10.0/SeqMcpServer.dll")); var clientTransport = new StdioClientTransport(new StdioClientTransportOptions { @@ -137,6 +137,8 @@ public async Task DisposeAsync() [Fact] public async Task SeqSearch_WithValidFilter_ReturnsEvents() { + await WriteTestEventAsync("Compatibility smoke event", "CompatibilitySmoke", true); + // Arrange - Get available tools var tools = await _mcpClient!.ListToolsAsync(); var seqSearchTool = tools.FirstOrDefault(t => t.Name == "SeqSearch"); @@ -147,7 +149,7 @@ public async Task SeqSearch_WithValidFilter_ReturnsEvents() "SeqSearch", new Dictionary { - ["filter"] = "*", + ["filter"] = "CompatibilitySmoke = true", ["count"] = 10 }); @@ -157,6 +159,24 @@ public async Task SeqSearch_WithValidFilter_ReturnsEvents() Assert.True(result.Content.Any()); } + [Fact] + public async Task SeqSearch_WithSeq2024_3WithoutScanLink_FallsBackToPagedEnumeration() + { + await WriteTestEventAsync("Compat fallback event", "CompatibilityFallback", true); + + var result = await _mcpClient!.CallToolAsync( + "SeqSearch", + new Dictionary + { + ["filter"] = "CompatibilityFallback = true", + ["count"] = 10 + }); + + Assert.NotNull(result); + Assert.NotNull(result.Content); + Assert.True(result.Content.Any()); + } + [Fact] public async Task SignalList_ReturnsSignals() { @@ -209,4 +229,18 @@ public async Task MCP_Client_CanListTools() Assert.Contains(tools, t => t.Name == "SignalList"); Assert.Equal(3, tools.Count); } -} \ No newline at end of file + + private async Task WriteTestEventAsync(string messageTemplate, string propertyName, bool propertyValue) + { + ArgumentNullException.ThrowIfNull(_seqUrl); + + using var httpClient = new HttpClient(); + var clef = $$""" + {"@t":"{{DateTimeOffset.UtcNow:O}}","@mt":"{{messageTemplate}}","{{propertyName}}":{{propertyValue.ToString().ToLowerInvariant()}}} + """; + + using var content = new StringContent(clef, System.Text.Encoding.UTF8, "application/vnd.serilog.clef"); + using var response = await httpClient.PostAsync($"{_seqUrl}/api/events/raw?clef", content); + response.EnsureSuccessStatusCode(); + } +} diff --git a/tests/SeqMcpServer.Tests/SeqMcpServer.Tests.csproj b/tests/SeqMcpServer.Tests/SeqMcpServer.Tests.csproj index c8c44e4..2eff103 100644 --- a/tests/SeqMcpServer.Tests/SeqMcpServer.Tests.csproj +++ b/tests/SeqMcpServer.Tests/SeqMcpServer.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false From 636b98328999749a56141155fbeeb4812d420231 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 08:20:48 +0000 Subject: [PATCH 2/3] Split Seq integration coverage into legacy fallback and modern Scan fixtures Agent-Logs-Url: https://github.com/aarondglover/seq-mcp-server/sessions/7f723b93-05bb-4421-999d-c5b17ddef143 Co-authored-by: aarondglover <8821892+aarondglover@users.noreply.github.com> --- .../McpToolsIntegrationTests.cs | 118 ++++++++++++++---- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs index 13875fc..8895061 100644 --- a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs +++ b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs @@ -8,19 +8,23 @@ namespace SeqMcpServer.Tests; -[Collection("McpIntegration")] -public class McpToolsIntegrationTests : IAsyncLifetime +public abstract class McpToolsIntegrationTestsBase : IAsyncLifetime { private IContainer? _seqContainer; private string? _seqUrl; private IHost? _mcpServerHost; private IMcpClient? _mcpClient; + protected abstract string SeqImageTag { get; } + + protected string SeqUrl => _seqUrl ?? throw new InvalidOperationException("Seq URL not initialized."); + protected IMcpClient McpClient => _mcpClient ?? throw new InvalidOperationException("MCP client not initialized."); + public async Task InitializeAsync() { - // Start Seq container - use stable version and proper configuration + // Start Seq container - version is configured by each derived test fixture _seqContainer = new ContainerBuilder() - .WithImage("datalust/seq:2024.3") // Use specific stable version + .WithImage($"datalust/seq:{SeqImageTag}") .WithPortBinding(80, true) // Map container port 80 (main API) to random host port .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment("SEQ_API_CANONICALURI", "http://localhost") @@ -102,7 +106,7 @@ public async Task InitializeAsync() // Create MCP client transport // Build path relative to the test assembly location - var testAssemblyLocation = Path.GetDirectoryName(typeof(McpToolsIntegrationTests).Assembly.Location)!; + var testAssemblyLocation = Path.GetDirectoryName(typeof(McpToolsIntegrationTestsBase).Assembly.Location)!; var serverDllPath = Path.GetFullPath(Path.Combine(testAssemblyLocation, "../../../../../src/SeqMcpServer/bin/Debug/net10.0/SeqMcpServer.dll")); var clientTransport = new StdioClientTransport(new StdioClientTransportOptions @@ -134,18 +138,17 @@ public async Task DisposeAsync() await _seqContainer.DisposeAsync(); } - [Fact] - public async Task SeqSearch_WithValidFilter_ReturnsEvents() + protected async Task SeqSearch_WithValidFilter_ReturnsEvents_Core() { await WriteTestEventAsync("Compatibility smoke event", "CompatibilitySmoke", true); // Arrange - Get available tools - var tools = await _mcpClient!.ListToolsAsync(); + var tools = await McpClient.ListToolsAsync(); var seqSearchTool = tools.FirstOrDefault(t => t.Name == "SeqSearch"); Assert.NotNull(seqSearchTool); // Act - Call the seq_search tool via MCP - var result = await _mcpClient!.CallToolAsync( + var result = await McpClient.CallToolAsync( "SeqSearch", new Dictionary { @@ -159,16 +162,15 @@ public async Task SeqSearch_WithValidFilter_ReturnsEvents() Assert.True(result.Content.Any()); } - [Fact] - public async Task SeqSearch_WithSeq2024_3WithoutScanLink_FallsBackToPagedEnumeration() + protected async Task SeqSearch_WithPropertyFilter_ReturnsEvents(string propertyName) { - await WriteTestEventAsync("Compat fallback event", "CompatibilityFallback", true); + await WriteTestEventAsync("Compatibility fixture event", propertyName, true); - var result = await _mcpClient!.CallToolAsync( + var result = await McpClient.CallToolAsync( "SeqSearch", new Dictionary { - ["filter"] = "CompatibilityFallback = true", + ["filter"] = $"{propertyName} = true", ["count"] = 10 }); @@ -177,16 +179,26 @@ public async Task SeqSearch_WithSeq2024_3WithoutScanLink_FallsBackToPagedEnumera Assert.True(result.Content.Any()); } - [Fact] - public async Task SignalList_ReturnsSignals() + protected async Task AssertScanLinkAvailabilityAsync(bool shouldExist) + { + using var httpClient = new HttpClient(); + var eventsApi = await httpClient.GetStringAsync($"{SeqUrl}/api/events"); + + if (shouldExist) + Assert.Contains("Scan", eventsApi, StringComparison.OrdinalIgnoreCase); + else + Assert.DoesNotContain("Scan", eventsApi, StringComparison.OrdinalIgnoreCase); + } + + protected async Task SignalList_ReturnsSignals_Core() { // Arrange - Get available tools - var tools = await _mcpClient!.ListToolsAsync(); + var tools = await McpClient.ListToolsAsync(); var signalListTool = tools.FirstOrDefault(t => t.Name == "SignalList"); Assert.NotNull(signalListTool); // Act - Call the signal_list tool via MCP - var result = await _mcpClient!.CallToolAsync("SignalList", new Dictionary()); + var result = await McpClient.CallToolAsync("SignalList", new Dictionary()); // Assert - Should return valid result Assert.NotNull(result); @@ -194,16 +206,15 @@ public async Task SignalList_ReturnsSignals() Assert.True(result.Content.Any()); } - [Fact] - public async Task SeqWaitForEvents_CanCaptureEvents() + protected async Task SeqWaitForEvents_CanCaptureEvents_Core() { // Arrange - Get available tools - var tools = await _mcpClient!.ListToolsAsync(); + var tools = await McpClient.ListToolsAsync(); var seqWaitTool = tools.FirstOrDefault(t => t.Name == "SeqWaitForEvents"); Assert.NotNull(seqWaitTool); // Act - Call the SeqWaitForEvents tool via MCP - var result = await _mcpClient!.CallToolAsync( + var result = await McpClient.CallToolAsync( "SeqWaitForEvents", new Dictionary { @@ -216,11 +227,10 @@ public async Task SeqWaitForEvents_CanCaptureEvents() // Note: Content might be empty if no events occurred during the wait period } - [Fact] - public async Task MCP_Client_CanListTools() + protected async Task MCP_Client_CanListTools_Core() { // Act - List available tools via MCP - var tools = await _mcpClient!.ListToolsAsync(); + var tools = await McpClient.ListToolsAsync(); // Assert - Should have our three tools Assert.NotNull(tools); @@ -244,3 +254,61 @@ private async Task WriteTestEventAsync(string messageTemplate, string propertyNa response.EnsureSuccessStatusCode(); } } + +[Collection("McpIntegration")] +public class McpToolsIntegrationLegacySeqTests : McpToolsIntegrationTestsBase +{ + protected override string SeqImageTag => "2024.3"; + + [Fact] + public async Task SeqSearch_WithValidFilter_ReturnsEvents() => + await SeqSearch_WithValidFilter_ReturnsEvents_Core(); + + [Fact] + public async Task SignalList_ReturnsSignals() => + await SignalList_ReturnsSignals_Core(); + + [Fact] + public async Task SeqWaitForEvents_CanCaptureEvents() => + await SeqWaitForEvents_CanCaptureEvents_Core(); + + [Fact] + public async Task MCP_Client_CanListTools() => + await MCP_Client_CanListTools_Core(); + + [Fact] + public async Task SeqSearch_WithSeq2024_3WithoutScanLink_FallsBackToPagedEnumeration() + { + await AssertScanLinkAvailabilityAsync(shouldExist: false); + await SeqSearch_WithPropertyFilter_ReturnsEvents("CompatibilityFallback"); + } +} + +[Collection("McpIntegration")] +public class McpToolsIntegrationModernSeqTests : McpToolsIntegrationTestsBase +{ + protected override string SeqImageTag => "2025.2"; + + [Fact] + public async Task SeqSearch_WithValidFilter_ReturnsEvents() => + await SeqSearch_WithValidFilter_ReturnsEvents_Core(); + + [Fact] + public async Task SignalList_ReturnsSignals() => + await SignalList_ReturnsSignals_Core(); + + [Fact] + public async Task SeqWaitForEvents_CanCaptureEvents() => + await SeqWaitForEvents_CanCaptureEvents_Core(); + + [Fact] + public async Task MCP_Client_CanListTools() => + await MCP_Client_CanListTools_Core(); + + [Fact] + public async Task SeqSearch_WithSeq2025_2WithScanLink_CoversDirectScanPath() + { + await AssertScanLinkAvailabilityAsync(shouldExist: true); + await SeqSearch_WithPropertyFilter_ReturnsEvents("CompatibilityScan"); + } +} From bed6b83ab46eb27f07e86bfd1188508398b1ffa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 09:07:08 +0000 Subject: [PATCH 3/3] Address review: probe Links.ContainsKey instead of catching, pin legacy image to 2024.3.13181 Co-authored-by: aarondglover <8821892+aarondglover@users.noreply.github.com> --- src/SeqMcpServer/Mcp/SeqTools.cs | 11 ++++++++--- tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs | 9 +-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/SeqMcpServer/Mcp/SeqTools.cs b/src/SeqMcpServer/Mcp/SeqTools.cs index 0f954e9..2ea046a 100644 --- a/src/SeqMcpServer/Mcp/SeqTools.cs +++ b/src/SeqMcpServer/Mcp/SeqTools.cs @@ -1,6 +1,7 @@ using ModelContextProtocol.Server; using Seq.Api.Model.Events; using Seq.Api.Model.Signals; +using Seq.Api.ResourceGroups; using SeqMcpServer.Services; using System.ComponentModel; using System.ComponentModel.DataAnnotations; @@ -35,7 +36,12 @@ public static async Task> SeqSearch( var conn = fac.Create(workspace); var events = new List(); - try + // Probe whether this Seq server exposes the Scan link (added in Seq 2024.4). + // SeqConnection caches the resource group, so this doesn't add a round-trip. + var eventsGroup = await ((ILoadResourceGroup)conn).LoadResourceGroupAsync("Events", ct); + var hasScanLink = eventsGroup.Links.ContainsKey("Scan"); + + if (hasScanLink) { await foreach (var evt in conn.Events.EnumerateAsync( filter: filter, @@ -46,9 +52,8 @@ public static async Task> SeqSearch( events.Add(evt); } } - catch (NotSupportedException ex) when (ex.Message.Contains("Scan", StringComparison.OrdinalIgnoreCase)) + else { - // Older or differently-configured Seq APIs may not expose the Scan link used by EnumerateAsync(). await foreach (var evt in conn.Events.PagedEnumerateAsync( unsavedSignal: null, signal: null, diff --git a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs index 8895061..d9520d2 100644 --- a/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs +++ b/tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs @@ -258,7 +258,7 @@ private async Task WriteTestEventAsync(string messageTemplate, string propertyNa [Collection("McpIntegration")] public class McpToolsIntegrationLegacySeqTests : McpToolsIntegrationTestsBase { - protected override string SeqImageTag => "2024.3"; + protected override string SeqImageTag => "2024.3.13181"; [Fact] public async Task SeqSearch_WithValidFilter_ReturnsEvents() => @@ -275,13 +275,6 @@ public async Task SeqWaitForEvents_CanCaptureEvents() => [Fact] public async Task MCP_Client_CanListTools() => await MCP_Client_CanListTools_Core(); - - [Fact] - public async Task SeqSearch_WithSeq2024_3WithoutScanLink_FallsBackToPagedEnumeration() - { - await AssertScanLinkAvailabilityAsync(shouldExist: false); - await SeqSearch_WithPropertyFilter_ReturnsEvents("CompatibilityFallback"); - } } [Collection("McpIntegration")]