Files
aryx/sidecar/tests/Eryx.AgentHost.Tests/SessionToolingBundleTests.cs
T
David KayaandCopilot e5878ba9e8 feat: auto-discover MCP servers from project and user config files
Scan .vscode/mcp.json, .mcp.json, .copilot/mcp.json (project-level) and
~/.copilot/mcp.json (user-level) for MCP server definitions. Discovered
servers require explicit user acceptance before activation.

Backend:
- Add ConfigScannerRegistry with per-format scanners and  substitution
- Add discovered tooling domain model with fingerprint-based change detection
- Integrate scanning into workspace load, project add, and project selection
- Merge accepted discovered MCPs into effective runtime tooling
- Extend sidecar contracts with env/headers for real-world MCP configs
- Add IPC channels for accept/dismiss/rescan operations

Frontend:
- Add DiscoveredToolingModal for reviewing pending MCP servers
- Auto-show modal when pending discoveries exist on load or project switch
- Add amber badge on sidebar project headers for pending discoveries
- Add discovered MCP section in Settings panel with accept/dismiss/rescan
- Group InlinePills tool dropdown by Workspace MCP / User MCP / Project MCP
- Pass effective project tooling (including accepted discoveries) to ChatPane

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-25 21:54:06 +01:00

113 lines
4.0 KiB
C#

using Eryx.AgentHost.Contracts;
using Eryx.AgentHost.Services;
using GitHub.Copilot.SDK;
namespace Eryx.AgentHost.Tests;
public sealed class SessionToolingBundleTests
{
[Fact]
public void BuildMcpServerConfigurations_MapsLocalAndRemoteServers()
{
IReadOnlyList<RunTurnMcpServerConfigDto> servers =
[
new()
{
Id = "mcp-local",
Name = "Local MCP",
Transport = "local",
Command = "node",
Args = ["server.js", "--stdio"],
Env = new Dictionary<string, string>
{
["DEBUG"] = "true",
},
Cwd = @"C:\workspace\repo",
Tools = ["git.status"],
TimeoutMs = 1500,
},
new()
{
Id = "mcp-remote",
Name = "Remote MCP",
Transport = "http",
Url = "https://example.com/mcp",
Headers = new Dictionary<string, string>
{
["Authorization"] = "Bearer token",
},
Tools = ["*"],
},
];
Dictionary<string, object> configurations = SessionToolingBundle.BuildMcpServerConfigurations(servers);
McpLocalServerConfig localConfig = Assert.IsType<McpLocalServerConfig>(configurations["Local MCP"]);
Assert.Equal("local", localConfig.Type);
Assert.Equal("node", localConfig.Command);
Assert.Equal(["server.js", "--stdio"], localConfig.Args);
KeyValuePair<string, string> localEnv = Assert.Single(localConfig.Env!);
Assert.Equal("DEBUG", localEnv.Key);
Assert.Equal("true", localEnv.Value);
Assert.Equal(@"C:\workspace\repo", localConfig.Cwd);
Assert.Equal(["git.status"], localConfig.Tools);
Assert.Equal(1500, localConfig.Timeout);
McpRemoteServerConfig remoteConfig = Assert.IsType<McpRemoteServerConfig>(configurations["Remote MCP"]);
Assert.Equal("http", remoteConfig.Type);
Assert.Equal("https://example.com/mcp", remoteConfig.Url);
KeyValuePair<string, string> remoteHeader = Assert.Single(remoteConfig.Headers!);
Assert.Equal("Authorization", remoteHeader.Key);
Assert.Equal("Bearer token", remoteHeader.Value);
Assert.Equal(["*"], remoteConfig.Tools);
}
[Fact]
public void BuildMcpServerConfigurations_DefaultsMissingToolsToWildcard()
{
IReadOnlyList<RunTurnMcpServerConfigDto> servers =
[
new()
{
Id = "mcp-local",
Transport = "local",
Command = "node",
Tools = [],
},
];
Dictionary<string, object> configurations = SessionToolingBundle.BuildMcpServerConfigurations(servers);
McpLocalServerConfig localConfig = Assert.IsType<McpLocalServerConfig>(configurations["mcp-local"]);
Assert.Equal(["*"], localConfig.Tools);
}
[Fact]
public void BuildMcpServerConfigurations_RejectsMissingTransportTargets()
{
InvalidOperationException localError = Assert.Throws<InvalidOperationException>(
() => SessionToolingBundle.BuildMcpServerConfigurations(
[
new RunTurnMcpServerConfigDto
{
Id = "mcp-local",
Name = "Local MCP",
Transport = "local",
},
]));
Assert.Contains("missing a command", localError.Message);
InvalidOperationException remoteError = Assert.Throws<InvalidOperationException>(
() => SessionToolingBundle.BuildMcpServerConfigurations(
[
new RunTurnMcpServerConfigDto
{
Id = "mcp-remote",
Name = "Remote MCP",
Transport = "sse",
},
]));
Assert.Contains("missing a URL", remoteError.Message);
}
}