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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,18 @@ The following tools are available through the MCP protocol:
- Returns: Snapshot of events captured during the wait period (may be empty if no events match)

- **`SignalList`** - List available signals (read-only)
- Parameters:
- Parameters:
- `workspace` (optional): Specific workspace to query
- Returns: List of signals with their definitions

- **`SeqConvertFilter`** - Convert fuzzy filter to strict filter expression
- Parameters:
- `fuzzyFilter` (required): Fuzzy search text (e.g., "error", "timeout")
- `workspace` (optional): Specific workspace to query
- Returns: Strict Seq filter expression for use in `SeqSearch`
- Use case: Help users write correct filter expressions
- Example: Convert "error" to a proper Seq filter expression

## Claude Desktop Integration

### Option 1: Using .NET Global Tool (Recommended)
Expand Down
61 changes: 61 additions & 0 deletions src/SeqMcpServer/Mcp/SeqTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,65 @@ public static async Task<List<SignalEntity>> SignalList(
throw;
}
}

/// <summary>
/// Convert a fuzzy filter expression to a strict Seq filter expression.
/// </summary>
/// <remarks>
/// Seq supports both "fuzzy" filters (like typing in the UI search box) and "strict" filters
/// (formal filter expressions). This tool converts fuzzy filters to strict ones, helping users
/// write correct filter expressions. For example, "error" becomes a proper filter expression.
/// The result includes whether the filter was interpreted as a text search and the reason if so.
/// </remarks>
/// <param name="fac">Factory for creating Seq connections</param>
/// <param name="fuzzyFilter">The fuzzy filter expression to convert (e.g., "error", "timeout")</param>
/// <param name="workspace">Optional workspace identifier for multi-tenant scenarios</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Conversion result including the strict expression and metadata about the conversion</returns>
[McpServerTool, Description("Convert a fuzzy filter expression to a strict Seq filter expression. Helps write correct filters for SeqSearch.")]
public static async Task<object> SeqConvertFilter(
SeqConnectionFactory fac,
[Required] string fuzzyFilter,
string? workspace = null,
CancellationToken ct = default)
{
try
{
var conn = fac.Create(workspace);
var result = await conn.Expressions.ToStrictAsync(fuzzyFilter, cancellationToken: ct);

if (result == null)
{
return new
{
strictExpression = fuzzyFilter,
matchedAsText = false,
reasonIfMatchedAsText = (string?)null
};
}

// Return all useful fields from ExpressionPart
return new
{
strictExpression = result.StrictExpression ?? fuzzyFilter,
matchedAsText = result.MatchedAsText,
reasonIfMatchedAsText = result.ReasonIfMatchedAsText
};
}
catch (OperationCanceledException)
{
// Return original filter on cancellation
return new
{
strictExpression = fuzzyFilter,
matchedAsText = false,
reasonIfMatchedAsText = (string?)null
};
}
catch (Exception)
{
// Re-throw to let MCP handle the error
throw;
}
}
}
6 changes: 3 additions & 3 deletions src/SeqMcpServer/SeqMcpServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

<!-- Package Metadata -->
<PackageId>SeqMcpServer</PackageId>
<Version>0.1.2</Version>
<AssemblyVersion>0.1.2.0</AssemblyVersion>
<FileVersion>0.1.2.0</FileVersion>
<Version>0.1.3</Version>
<AssemblyVersion>0.1.3.0</AssemblyVersion>
<FileVersion>0.1.3.0</FileVersion>
<Authors>willibrandon</Authors>
<Description>MCP server that enables AI assistants to query Seq logs using natural language</Description>
<PackageProjectUrl>https://github.com/willibrandon/seq-mcp-server</PackageProjectUrl>
Expand Down
51 changes: 49 additions & 2 deletions tests/SeqMcpServer.Tests/McpToolsIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,58 @@ public async Task MCP_Client_CanListTools()
// Act - List available tools via MCP
var tools = await _mcpClient!.ListToolsAsync();

// Assert - Should have our three tools
// Assert - Should have our four tools
Assert.NotNull(tools);
Assert.Contains(tools, t => t.Name == "SeqSearch");
Assert.Contains(tools, t => t.Name == "SeqWaitForEvents");
Assert.Contains(tools, t => t.Name == "SignalList");
Assert.Equal(3, tools.Count);
Assert.Contains(tools, t => t.Name == "SeqConvertFilter");
Assert.Equal(4, tools.Count);
}

[Fact]
public async Task SeqConvertFilter_ConvertsFilterSuccessfully()
{
// Act - Convert a fuzzy filter to strict
var result = await _mcpClient!.CallToolAsync(
"SeqConvertFilter",
new Dictionary<string, object?>
{
["fuzzyFilter"] = "error"
});

// Assert - Should return a converted filter with all three fields
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.True(result.Content.Any());
Assert.False(result.IsError);

// Serialize the content to JSON string to verify structure
var contentJson = JsonSerializer.Serialize(result.Content.First());
Assert.Contains("strictExpression", contentJson);
Assert.Contains("matchedAsText", contentJson);
Assert.Contains("reasonIfMatchedAsText", contentJson);
}

[Fact]
public async Task SeqConvertFilter_WithTextSearch_ReturnsMetadata()
{
// Act - Convert a fuzzy filter that will be treated as text search
var result = await _mcpClient!.CallToolAsync(
"SeqConvertFilter",
new Dictionary<string, object?>
{
["fuzzyFilter"] = "error timeout" // Invalid syntax, will be text search
});

// Assert - Should return result with matchedAsText=true and reason
Assert.NotNull(result);
Assert.NotNull(result.Content);
Assert.True(result.Content.Any());
Assert.False(result.IsError);

var contentJson = JsonSerializer.Serialize(result.Content.First());
Assert.Contains("\"matchedAsText\":true", contentJson);
Assert.Contains("reasonIfMatchedAsText", contentJson);
}
}