Spec: the complete environment goes to the SDK with InheritEnvironmentVariables=false instead of nulls; the cache key also covers cwd and prefix.
65 lines
3.1 KiB
C#
65 lines
3.1 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using ModelContextProtocol.Client;
|
|
using Volcanic.AgenticCore.Abstractions.Processes;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Mcp;
|
|
|
|
/// <summary>Turns a server config into a spawn request and the SDK's transport options.</summary>
|
|
internal static partial class McpLaunch
|
|
{
|
|
public const string WorkspaceToken = "${workspace}";
|
|
public static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(5);
|
|
|
|
public static string ExpandWorkspace(string value, string workspace) =>
|
|
value.Replace(WorkspaceToken, workspace, StringComparison.Ordinal);
|
|
|
|
/// <summary><c>${workspace}</c>, then <c>${env:NAME}</c> from the host (unset → empty). Only env values get the second.</summary>
|
|
public static string ExpandEnvValue(string value, string workspace, Func<string, string?> getEnv) =>
|
|
EnvReference().Replace(ExpandWorkspace(value, workspace), m => getEnv(m.Groups[1].Value) ?? "");
|
|
|
|
public static ProcessSpawnRequest BuildRequest(string name, McpServerConfig config, string workspace, Func<string, string?> getEnv) => new()
|
|
{
|
|
Purpose = ProcessPurpose.McpServer,
|
|
Command = ExpandWorkspace(config.Command, workspace),
|
|
Arguments = [.. config.Args.Select(a => ExpandWorkspace(a, workspace))],
|
|
Cwd = config.Cwd is { Length: > 0 } cwd ? ExpandWorkspace(cwd, workspace) : workspace,
|
|
Environment = config.Env.Count == 0
|
|
? null
|
|
: config.Env.ToDictionary(kv => kv.Key, kv => kv.Value is null ? null : ExpandEnvValue(kv.Value, workspace, getEnv), StringComparer.Ordinal),
|
|
Origin = name,
|
|
};
|
|
|
|
/// <summary>The spawn's environment is complete (process policy), so the SDK must not merge the host's back in.</summary>
|
|
public static StdioClientTransportOptions TransportOptions(string name, ProcessSpawnContext spawn, Action<string> onStderrLine) => new()
|
|
{
|
|
Name = name,
|
|
Command = spawn.Command,
|
|
Arguments = [.. spawn.Arguments],
|
|
WorkingDirectory = spawn.Cwd,
|
|
InheritEnvironmentVariables = false,
|
|
EnvironmentVariables = new Dictionary<string, string?>(spawn.Environment.Select(kv => KeyValuePair.Create(kv.Key, (string?)kv.Value))),
|
|
ShutdownTimeout = ShutdownTimeout,
|
|
StandardErrorLines = onStderrLine,
|
|
};
|
|
|
|
/// <summary>Server name plus a digest of the unexpanded command, args, env, cwd and prefix: every chat shares it, and no host secret enters it.</summary>
|
|
public static string CacheKey(string name, McpServerConfig config)
|
|
{
|
|
var canonical = JsonSerializer.Serialize(new
|
|
{
|
|
config.Command,
|
|
config.Args,
|
|
Env = config.Env.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => new[] { kv.Key, kv.Value }).ToArray(),
|
|
config.Cwd,
|
|
config.ToolPrefix,
|
|
});
|
|
return $"{name}:{Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(canonical)))}";
|
|
}
|
|
|
|
[GeneratedRegex(@"\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}")]
|
|
private static partial Regex EnvReference();
|
|
}
|