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
42 changes: 40 additions & 2 deletions src/SeqCli/Cli/Commands/Mcp/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
// limitations under the License.

using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Protocol;
using Seq.Api;
using Seq.Api.Model.Settings;
using SeqCli.Api;
using SeqCli.Cli.Features;
using SeqCli.Config;
Expand Down Expand Up @@ -65,10 +70,25 @@ protected override async Task<int> Run()

var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { DisableDefaults = true });
builder.Services.AddSerilog();
builder.Services.AddSingleton(_ => SeqConnectionFactory.Connect(_connection, config));
var connection = SeqConnectionFactory.Connect(_connection, config);
var instanceTitle = await TryGetInstanceTitleAsync(connection);

builder.Services.AddSingleton(connection);
builder.Services.AddSingleton<McpSession>();
builder.Services
.AddMcpServer()
.AddMcpServer(options =>
{
var identity = instanceTitle != null ? $"Seq ({instanceTitle})" : "Seq";
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
options.ServerInfo = new Implementation
{
Name = identity,
Version = assemblyName?.Version?.ToString() ?? "0.0.0"
};

if (instanceTitle != null)
options.ServerInstructions = $"Connected to {identity}";
})
.WithStdioServerTransport()
.WithTools([
typeof(SearchTools),
Expand All @@ -91,4 +111,22 @@ protected override async Task<int> Run()
}
return 0;
}

static async Task<string?> TryGetInstanceTitleAsync(SeqConnection connection)
{
try
{
// Not really any good options, here - we might have a 1-2 second deadline for first-turn availability,
// and 30-60 second absolute deadline. It's likely the call will be subsecond, but if we miss the first
// turn, might as well keep waiting, the tool won't work if Seq is down, anyway.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var setting = await connection.Settings.FindNamedAsync(SettingName.InstanceTitle, cts.Token);
return setting.Value is string title && !string.IsNullOrWhiteSpace(title) ? title.Trim() : null;
}
catch (Exception ex)
{
Log.Information(ex, "Could not retrieve the Seq instance title");
return null;
}
}
}
34 changes: 34 additions & 0 deletions test/SeqCli.EndToEnd/Mcp/McpInstanceTitleTestCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading.Tasks;
using ModelContextProtocol.Client;
using Seq.Api;
using Seq.Api.Model.Settings;
using SeqCli.EndToEnd.Support;
using Serilog;
using Xunit;

namespace SeqCli.EndToEnd.Mcp;

// ReSharper disable once UnusedType.Global
public class McpInstanceTitleTestCase : ICliTestCase
{
public async Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner)
{
const string instanceTitle = "Test Instance";

var setting = await connection.Settings.FindNamedAsync(SettingName.InstanceTitle);
setting.Value = instanceTitle;
await connection.Settings.UpdateAsync(setting);

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "seqcli mcp run",
Command = "dotnet",
Arguments = [TestConfiguration.TestedBinary, "mcp", "run", $"--server={connection.Client.ServerUrl}"]
});

await using var client = await McpClient.CreateAsync(transport);

Assert.EndsWith($" ({instanceTitle})", client.ServerInfo.Name);
Assert.Equal($"Connected to Seq ({instanceTitle})", client.ServerInstructions);
}
}