- 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
91 lines
4.0 KiB
C#
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;
|
|
}
|
|
}
|