Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4d4940c
Mark SeqCli.EndToEnd as a non-test project so that it's not picked up…
May 26, 2026
184952b
seqcli skills install command
May 26, 2026
b3a4062
More skill WIP
May 29, 2026
f1784c6
More skill WIP
May 29, 2026
588230d
More skill and tool WIP
May 29, 2026
229bee3
More skill and tool WIP
May 29, 2026
f22d041
More fixes
May 29, 2026
9cb90ee
More WIP
May 29, 2026
1b47da1
Minor tweaks
May 29, 2026
aaaeb41
Remove .claude and add to .gitignore
May 30, 2026
2229605
Minimal tests for identifier generation
May 30, 2026
76a6cec
OTel property name accessor gotcha
May 30, 2026
c19d839
More gotchas; these are a useful catalog of papercuts we might actual…
May 30, 2026
9f131b9
Get the (implemented) integration tests running.
May 30, 2026
8ca84d5
Added a simple MCP end-to-end test case.
May 30, 2026
ae29b25
Add tests for SeqSyntaxFormatter.
May 30, 2026
093bccd
A first cut `mcp install` command.
May 30, 2026
c2eb030
Tidy up.
May 31, 2026
4d4f68f
Increase test coverage for McpSession.
May 31, 2026
cb5ed21
Add seq_new_session endpoint to clear state.
May 31, 2026
3380f5f
Factor some orthogonal API helpers out of the (crowded) tool class
May 31, 2026
4915842
Skill updates, add `mcp install` support for Qwen Code.
May 31, 2026
aa344ae
Use the example trace id consistently
May 31, 2026
6dd69db
More skill tweaks
May 31, 2026
40a1ae4
More skill tweaks
May 31, 2026
0e2de67
Update SKILL.md to remove deprecated operators
nblumhardt May 31, 2026
9cf0288
Review feedback, reduce SKILL.md token count by dropping out table/gr…
nblumhardt Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,7 @@ __pycache__/
global.json

.DS_Store/

.claude/
.qwen/
.agents/
52 changes: 52 additions & 0 deletions src/SeqCli/Cli/Commands/Mcp/InstallCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Threading.Tasks;
using SeqCli.Mcp;
using SeqCli.Util;

namespace SeqCli.Cli.Commands.Mcp;

[Command("mcp", "install", "Install or update the Seq MCP server for an agent",
Example = "seqcli mcp install --global --agent claude")]
class InstallCommand : Command
{
bool _global;
string? _agent;
string? _profile;

public InstallCommand()
{
Options.Add(
"g|global",
"Install for the current user globally; the default is to install into the current project directory",
_ => _global = true);

Options.Add(
"a=|agent=",
"The agent name to install the MCP server for; the default is the generic name `agents`",
t => _agent = ArgumentString.Normalize(t));

Options.Add(
"profile=",
"A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used",
v => _profile = ArgumentString.Normalize(v));
}

protected override Task<int> Run()
{
McpServerInstaller.Install(_agent?.ToLowerInvariant(), _global, _profile);
return Task.FromResult(0);
}
}
85 changes: 85 additions & 0 deletions src/SeqCli/Cli/Commands/Mcp/RunCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Threading.Tasks;
using Autofac.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
using SeqCli.Mcp;
using SeqCli.Mcp.Tools.Search;
using Serilog;

namespace SeqCli.Cli.Commands.Mcp;

[Command("mcp", "run", "Run an MCP (Model Context Protocol) server on STDIO")]
class RunCommand: Command
{
readonly ConnectionFeature _connection;
readonly StoragePathFeature _storagePath;
bool _debug;

public RunCommand()
{
_connection = Enable<ConnectionFeature>();
_storagePath = Enable<StoragePathFeature>();
Options.Add("debug", "Write diagnostic messages from the MCP server back through the connection",
_ => _debug = true);
}

protected override async Task<int> Run()
{
var config = RuntimeConfigurationLoader.Load(_storagePath);

if (_debug)
{
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Application", "seqcli mcp run")
.WriteTo.Seq(config.Connection.ServerUrl, apiKey: config.Connection.DecodeApiKey(config.Encryption.DataProtector()))
.CreateLogger();

Log.Information("Seq MCP server starting up");
}

try
{
var builder = Host.CreateApplicationBuilder();
builder.ConfigureContainer(new AutofacServiceProviderFactory());
builder.Services.AddSerilog();
builder.Services.AddSingleton(_ => SeqConnectionFactory.Connect(_connection, config));
builder.Services.AddSingleton<McpSession>();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools([
typeof(SearchAndQueryToolType)
]);

await builder.Build().RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception");
return 1;
}
finally
{
await Log.CloseAndFlushAsync();
}
return 0;
}
}
6 changes: 3 additions & 3 deletions src/SeqCli/Cli/Commands/SearchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ protected override async Task<int> Run()
}
}

LogEvent ToSerilogEvent(EventEntity evt)
internal static LogEvent ToSerilogEvent(EventEntity evt)
{
return new LogEvent(
DateTimeOffset.ParseExact(evt.Timestamp, "o", CultureInfo.InvariantCulture).ToLocalTime(),
Expand All @@ -149,12 +149,12 @@ static MessageTemplateToken ToMessageTemplateToken(MessageTemplateTokenPart toke
return new PropertyToken(token.PropertyName, token.RawText ?? $"{{{token.PropertyName}}}");
}

LogEventProperty CreateProperty(string name, object value)
static LogEventProperty CreateProperty(string name, object value)
{
return LogEventPropertyFactory.SafeCreate(name, CreatePropertyValue(value));
}

LogEventPropertyValue CreatePropertyValue(object value)
static LogEventPropertyValue CreatePropertyValue(object value)
{
switch (value)
{
Expand Down
46 changes: 46 additions & 0 deletions src/SeqCli/Cli/Commands/Skills/InstallCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Threading.Tasks;
using SeqCli.Skills;
using SeqCli.Util;

namespace SeqCli.Cli.Commands.Skills;

[Command("skills", "install", "Install or update Seq agent skills",
Example = "seqcli skills install --global --agent claude")]
class InstallCommand : Command
{
bool _global;
string? _agent;

public InstallCommand()
{
Options.Add(
"g|global",
"Install skills globally, to `~/.{agent}/skills`; the default is to install locally, in `./{agent}/skills`",
_ => _global = true);

Options.Add(
"a=|agent=",
"The agent name to install skills for; the default is the generic name `agents`",
t => _agent = ArgumentString.Normalize(t));
}

protected override Task<int> Run()
{
SkillInstaller.Install(_agent?.ToLowerInvariant(), _global);
return Task.FromResult(0);
}
}
2 changes: 1 addition & 1 deletion src/SeqCli/Ingestion/LogShipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ static async Task<BatchResult> ReadBatchAsync(
if (isLast || batch.Count != 0 || totalWaitMS > maxWaitMS)
break;

// Nothing to to ship; wait to try to fill a batch.
// Nothing to ship; wait to try to fill a batch.
await Task.Delay(idleWaitMS);
totalWaitMS += idleWaitMS;
continue;
Expand Down
50 changes: 50 additions & 0 deletions src/SeqCli/Mcp/Data/DataResourceGroupHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Seq.Api;
using Seq.Api.Model.Data;

namespace SeqCli.Mcp.Data;

public static class DataResourceGroupHelper
{
static readonly JsonSerializer Serializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Culture = CultureInfo.InvariantCulture,
FloatParseHandling = FloatParseHandling.Decimal,
});

public static async Task<QueryResultPart> QueryPreserveErrorResponsesAsync(SeqConnection connection, string query, CancellationToken cancellationToken = default)
{
// Unfortunately, the `Data.QueryAsync()` API throws when the server 400s, making this case tricky. Suggests
Comment thread
nblumhardt marked this conversation as resolved.
// we should make some API client improvements...
var request = new HttpRequestMessage
{
RequestUri = new Uri("api/data?q=" + Uri.EscapeDataString(query), UriKind.Relative),
Method = HttpMethod.Post, Content = new StringContent("{}", new UTF8Encoding(false), "application/json")
};
var response = await connection.Client.HttpClient.SendAsync(request, cancellationToken);
return Serializer.Deserialize<QueryResultPart>(
new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken))))!;
}
}
105 changes: 105 additions & 0 deletions src/SeqCli/Mcp/Data/QueryResultHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright © Datalust and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using Seq.Api.Model.Data;

namespace SeqCli.Mcp.Data;

public static class QueryResultHelper
{
/// <summary>
/// Convert <paramref name="result"/> into a flat table. Seq reduces browser-side processing and optimizes
/// response sizes by constructing result trees for some grouped/time-sliced query results.
/// </summary>
public static void Flatten(QueryResultPart result, Action<IEnumerable<object?>> writeRow)
Comment thread
nblumhardt marked this conversation as resolved.
{
if (result.Error != null)
return;

if (result.Rows != null)
{
writeRow(result.Columns!);
foreach (var row in result.Rows)
{
writeRow(row);
}
}
else if (result.Slices != null)
{
writeRow(new object[] {"time"}.Concat(result.Columns!));

var empty = result.Columns!.Select(_ => "").ToArray();
foreach (var slice in result.Slices)
{
var any = false;
foreach (var row in slice.Rows)
{
any = true;
writeRow(new object[] { DateTimeOffset.Parse(slice.Time).UtcDateTime }.Concat(row));
}
if (!any)
{
writeRow(new object[] { DateTimeOffset.Parse(slice.Time).UtcDateTime }.Concat(empty));
}
}
}
else if (result.Series != null)
{
writeRow(MergeColumns(result.Columns!, result.Series.FirstOrDefault()));
foreach (var series in result.Series)
{
foreach (var slice in series.Slices)
{
var empty = result.Columns!.Take(series.Key.Length).Select(_ => (object?)null).ToArray();
var any = false;
foreach (var row in slice.Rows)
{
any = true;
writeRow(series.Key.Concat([DateTimeOffset.Parse(slice.Time).UtcDateTime]).Concat(row));
}
if (!any)
{
writeRow(series.Key.Concat([DateTimeOffset.Parse(slice.Time).UtcDateTime]).Concat(empty));
}
}
}
}
else
{
throw new NotImplementedException("Query result set does not conform to any expected pattern.");
}
}

static IEnumerable<object> MergeColumns(string[] columns, TimeseriesPart? firstSeries)
{
if (firstSeries == null)
yield break;

var i = 0;
for (; i < firstSeries.Key.Length; ++i)
{
yield return columns[i];
}

yield return "time";

for (; i < columns.Length; ++i)
{
yield return columns[i];
}
}
}
Loading
Loading