Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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_<WORKSPACE>`: Optional workspace-specific API keys (e.g., `SEQ_API_KEY_PRODUCTION`)
- `SEQ_API_KEY`: API key for accessing Seq (required)
- `SEQ_API_KEY_<WORKSPACE>`: 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

Expand All @@ -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
Expand Down Expand Up @@ -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.
MIT License - see [LICENSE](LICENSE) file for details.
12 changes: 11 additions & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
- Use HTTPS for all connections
45 changes: 39 additions & 6 deletions src/SeqMcpServer/Mcp/SeqTools.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,14 +35,46 @@ public static async Task<List<EventEntity>> SeqSearch(
{
var conn = fac.Create(workspace);
var events = new List<EventEntity>();
await foreach (var evt in conn.Events.EnumerateAsync(
filter: filter,
count: count,
render: true,
cancellationToken: ct).WithCancellation(ct))

// 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)
{
events.Add(evt);
await foreach (var evt in conn.Events.EnumerateAsync(
filter: filter,
count: count,
render: true,
cancellationToken: ct).WithCancellation(ct))
{
events.Add(evt);
}
}
else
{
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)
Expand Down
4 changes: 2 additions & 2 deletions src/SeqMcpServer/SeqMcpServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

Expand Down Expand Up @@ -37,7 +37,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.1" />
<PackageReference Include="Seq.Api" Version="2025.2.0" />
<PackageReference Include="Seq.Api" Version="2025.2.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
</ItemGroup>
Expand Down
141 changes: 118 additions & 23 deletions tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up on the modern-fixture CI failure: Seq 2025.x refuses to start unless either an admin password is supplied or SEQ_FIRSTRUN_NOAUTHENTICATION=true is set in the container env, so the container exits immediately and the readiness loop just hits "Connection refused" through the full retry budget. Adding .WithEnvironment("SEQ_FIRSTRUN_NOAUTHENTICATION", "true") here should unblock CI.

Tangentially — the modern fixture uses datalust/seq:2025.2 (floating), same drift concern that motivated pinning 2024.3.13181. Worth pinning a specific patch like 2025.2.16202 while you're in here.

.WithPortBinding(80, true) // Map container port 80 (main API) to random host port
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SEQ_API_CANONICALURI", "http://localhost")
Expand Down Expand Up @@ -102,8 +106,8 @@ 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 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
{
Expand Down Expand Up @@ -134,20 +138,21 @@ 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<string, object?>
{
["filter"] = "*",
["filter"] = "CompatibilitySmoke = true",
["count"] = 10
});

Expand All @@ -157,33 +162,59 @@ public async Task SeqSearch_WithValidFilter_ReturnsEvents()
Assert.True(result.Content.Any());
}

[Fact]
public async Task SignalList_ReturnsSignals()
protected async Task SeqSearch_WithPropertyFilter_ReturnsEvents(string propertyName)
{
await WriteTestEventAsync("Compatibility fixture event", propertyName, true);

var result = await McpClient.CallToolAsync(
"SeqSearch",
new Dictionary<string, object?>
{
["filter"] = $"{propertyName} = true",
["count"] = 10
});

Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.True(result.Content.Any());
}

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<string, object?>());
var result = await McpClient.CallToolAsync("SignalList", new Dictionary<string, object?>());

// Assert - Should return valid result
Assert.NotNull(result);
Assert.NotNull(result.Content);
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<string, object?>
{
Expand All @@ -196,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);
Expand All @@ -209,4 +239,69 @@ public async Task MCP_Client_CanListTools()
Assert.Contains(tools, t => t.Name == "SignalList");
Assert.Equal(3, tools.Count);
}
}

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();
}
}

[Collection("McpIntegration")]
public class McpToolsIntegrationLegacySeqTests : McpToolsIntegrationTestsBase
{
protected override string SeqImageTag => "2024.3.13181";

[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();
}

[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");
}
}
Loading
Loading