Files
VolcanicAgenticCore/tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/Remote/RemoteServersFileTests.cs
EugeneTes 891a644590 feat(mcp): remote MCP servers per chat with sealed keys and owner-only commands
Streamable HTTP/SSE servers from the workspace's .volcanic/mcp/servers.json.
Headers are sealed with AES-GCM; the chat and the visible fields are the
associated data, so only /mcp_add|remove|tools change servers. Tools
<server>__<tool> go live in the session. An unreachable server is skipped
and reported in the system prompt. Keys are redacted from tool results, and
Telegram deletes messages that carry one.
2026-10-09 21:11:01 +00:00

92 lines
3.8 KiB
C#

using System.Text.Json.Nodes;
using Volcanic.AgenticCore.Extensions.Mcp.Remote;
namespace Volcanic.AgenticCore.Extensions.Tests.Mcp.Remote;
public sealed class RemoteServersFileTests : IDisposable
{
private readonly string _workspace = Path.Combine(Path.GetTempPath(), "volcanic-mcp-file", Guid.NewGuid().ToString("N"));
private readonly RemoteServerCipher _cipher = new("secret-one", "telegram-1");
private static RemoteServerEntry Entry(string name = "notes") =>
new(name, "https://example.com/mcp", RemoteTransport.Auto, ["echo"], new Dictionary<string, string> { ["Authorization"] = "Bearer abcdef123456" });
public void Dispose()
{
try { Directory.Delete(_workspace, true); } catch (IOException) { } catch (UnauthorizedAccessException) { }
}
[Fact]
public void Round_trip_keeps_everything_and_the_file_has_no_key()
{
RemoteServersFile.Write(_workspace, _cipher, [Entry()]);
var read = RemoteServersFile.Read(_workspace, _cipher);
var entry = Assert.Single(read.Servers);
Assert.Equal("Bearer abcdef123456", entry.Headers["authorization"]);
Assert.Equal(["echo"], entry.Tools);
Assert.Empty(read.Problems);
Assert.DoesNotContain("abcdef123456", File.ReadAllText(RemoteServersFile.PathFor(_workspace)), StringComparison.Ordinal);
Assert.DoesNotContain("abcdef123456", entry.ToString(), StringComparison.Ordinal);
if (!OperatingSystem.IsWindows())
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(RemoteServersFile.PathFor(_workspace)));
}
[Theory]
[InlineData("url", "https://attacker.example/mcp")]
[InlineData("tools", null)]
[InlineData("name", "other")]
public void Changing_a_visible_field_breaks_the_seal(string field, string? value)
{
RemoteServersFile.Write(_workspace, _cipher, [Entry()]);
var path = RemoteServersFile.PathFor(_workspace);
var root = JsonNode.Parse(File.ReadAllText(path))!;
root["servers"]![0]![field] = value;
File.WriteAllText(path, root.ToJsonString());
var read = RemoteServersFile.Read(_workspace, _cipher);
Assert.Empty(read.Servers);
Assert.Contains("not added by /mcp_add", Assert.Single(read.Problems), StringComparison.Ordinal);
}
[Fact]
public void Another_chat_or_another_secret_cannot_open_it()
{
RemoteServersFile.Write(_workspace, _cipher, [Entry()]);
Assert.Empty(RemoteServersFile.Read(_workspace, new RemoteServerCipher("secret-one", "telegram-2")).Servers);
Assert.Empty(RemoteServersFile.Read(_workspace, new RemoteServerCipher("secret-two", "telegram-1")).Servers);
}
[Theory]
[InlineData("ftp://example.com/mcp")]
[InlineData("https://user:pass@example.com/mcp")]
[InlineData("not a url")]
public void Bad_urls_are_refused(string url) => Assert.NotNull(RemoteServersFile.CheckUrl(url));
[Fact]
public void Display_url_drops_the_query() =>
Assert.Equal("https://example.com/mcp", RemoteServersFile.DisplayUrl("https://example.com/mcp?key=abc"));
[Theory]
[InlineData("tok_123", "Authorization", "Bearer tok_123")]
[InlineData("X-Api-Key: k-123", "X-Api-Key", "k-123")]
public void Key_answers_become_headers(string answer, string header, string value)
{
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Assert.Null(RemoteMcpCommands.ParseKeyAnswer(answer, headers));
Assert.Equal(value, headers[header]);
}
[Fact]
public void Dash_means_no_key_and_spaces_are_refused()
{
var headers = new Dictionary<string, string>();
Assert.Null(RemoteMcpCommands.ParseKeyAnswer("-", headers));
Assert.Empty(headers);
Assert.NotNull(RemoteMcpCommands.ParseKeyAnswer("two words", headers));
}
}