Files
EugeneTes 2a201f7d95 fix(mcp): address review findings
- redact /mcp output, and stderr lines as they arrive (log, errors, tail)
- take host servers by name: configuration binding sorts keys anyway
- dispose kills the tree only without an exit code and only if the pid's
  start time matches the spawn (no kill of a reused pid)
- idle timer generation guards stale callbacks; catalogue check runs in
  the background with a 10 s cap; shutdown stops servers concurrently
- stop resets a failed status; /mcp shows an exited process; ordinal names
- spec: ${workspace} in command, ordering, dispose and /mcp wording
2026-09-26 21:18:12 +02:00

91 lines
4.0 KiB
C#

using Microsoft.Extensions.Logging;
namespace Volcanic.AgenticCore.Extensions.Mcp;
/// <summary>One configured server as a session sees it: config, prompt section and whether it can run.</summary>
internal sealed record McpServerEntry
{
public required string Name { get; init; }
public required McpServerConfig Config { get; init; }
/// <summary>Key in <c>SystemPromptOptions.Sections</c>.</summary>
public required string SectionKey { get; init; }
/// <summary>The prompt section given whether a <c>send</c> tool exists; null for none.</summary>
public required Func<bool, string?> PromptSection { get; init; }
public bool Disabled { get; init; }
public string? DisabledHint { get; init; }
/// <summary>Validation errors; a server with a problem is listed but never started.</summary>
public string? Problem { get; init; }
public static McpServerEntry For(string name, McpServerConfig config)
{
ArgumentNullException.ThrowIfNull(config);
var errors = config.Validate(name);
return new()
{
Name = name,
Config = config,
SectionKey = name,
PromptSection = _ => config.PromptSection,
Disabled = !config.Enabled,
Problem = errors.Count == 0 ? null : string.Join(" ", errors),
};
}
public static McpServerEntry Preset(PlaywrightOptions options) => new()
{
Name = PlaywrightPreset.ServerName,
Config = PlaywrightPreset.ToServer(options),
SectionKey = PlaywrightPreset.SectionKey,
PromptSection = hasSend => PlaywrightPreset.PromptSection(hasSend),
};
public static McpServerEntry DisabledPreset() => new()
{
Name = PlaywrightPreset.ServerName,
Config = new McpServerConfig(),
SectionKey = PlaywrightPreset.SectionKey,
PromptSection = _ => null,
Disabled = true,
DisabledHint = "set PLAYWRIGHT_ENABLED=true",
};
}
/// <summary>Merges the configuration sources. The first source wins a server name.</summary>
internal static class McpServerSet
{
/// <summary>
/// Host servers by name, then the Playwright preset when enabled, then the user settings in file order. A taken
/// name is skipped with a log line. A disabled preset is listed last, only while <c>playwright</c> is still free.
/// Host servers are sorted because configuration binding sorts keys anyway (<c>ConfigurationKeyComparer</c>), so
/// appsettings order can't be kept; sorting here gives options built in code the same rule.
/// </summary>
public static IReadOnlyList<McpServerEntry> Resolve(McpOptions options, IEnumerable<KeyValuePair<string, McpServerConfig>> userServers, ILogger logger)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(userServers);
ArgumentNullException.ThrowIfNull(logger);
var entries = new List<McpServerEntry>();
var names = new HashSet<string>(StringComparer.Ordinal);
var hostServers = options.Servers
.OrderBy(s => s.Key, StringComparer.OrdinalIgnoreCase)
.ThenBy(s => s.Key, StringComparer.Ordinal);
foreach (var (name, config) in hostServers)
if (names.Add(name)) entries.Add(McpServerEntry.For(name, config));
if (options.Playwright.IsEnabled)
{
if (names.Add(PlaywrightPreset.ServerName)) entries.Add(McpServerEntry.Preset(options.Playwright));
else logger.LogWarning("MCP server 'playwright' is configured explicitly; the Playwright preset is ignored.");
}
foreach (var (name, config) in userServers)
{
if (names.Add(name)) entries.Add(McpServerEntry.For(name, config));
else logger.LogInformation("MCP server '{Name}' in the user settings is ignored: the host configuration defines it.", name);
}
if (names.Add(PlaywrightPreset.ServerName)) entries.Add(McpServerEntry.DisabledPreset());
return entries;
}
}