feat: add server-level MCP auto-approval for wildcard tool servers

MCP servers configured with empty tools arrays (wildcard) now appear in the
auto-approval pill with a server-level toggle. When toggled, a server-level
approval key (mcp_server:<name>) is added to autoApprovedToolNames. The
sidecar matches this key against PermissionRequestMcp.ServerName to auto-
approve all tools from that server without needing individual tool names.

- Add buildMcpServerApprovalKey/isMcpServerApprovalKey helpers
- Create approval groups for all MCP servers including empty-tools ones
- Add serverApprovalKey to ApprovalToolGroup for server-level toggles
- Update sidecar RequiresToolCallApproval to check server-level keys
- Include server-level keys in listApprovalToolNames for pruning safety
- Add tests for both shared domain and sidecar approval matching

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-28 18:34:19 +01:00
co-authored by Copilot
parent 53a08e0ed4
commit 3937904548
6 changed files with 210 additions and 25 deletions
@@ -57,9 +57,10 @@ internal sealed class CopilotApprovalCoordinator
{
string? toolName = ResolveApprovalToolName(request, toolNamesByCallId);
string? autoApprovedToolName = ResolveAutoApprovedToolName(request);
string? mcpServerApprovalKey = ResolveMcpServerApprovalKey(request);
string? approvalCacheKey = ResolveApprovalCacheKey(toolName, autoApprovedToolName);
if (IsToolApprovedForRequest(command.RequestId, approvalCacheKey)
|| !RequiresToolCallApproval(command.Pattern.ApprovalPolicy, agent.Id, toolName, autoApprovedToolName))
|| !RequiresToolCallApproval(command.Pattern.ApprovalPolicy, agent.Id, toolName, autoApprovedToolName, mcpServerApprovalKey))
{
return CreateApprovalResult(PermissionRequestResultKind.Approved);
}
@@ -227,7 +228,8 @@ internal sealed class CopilotApprovalCoordinator
ApprovalPolicyDto? approvalPolicy,
string agentId,
string? toolName,
string? autoApprovedToolName = null)
string? autoApprovedToolName = null,
string? mcpServerApprovalKey = null)
{
if (approvalPolicy?.Rules is null || approvalPolicy.Rules.Count == 0)
{
@@ -245,7 +247,8 @@ internal sealed class CopilotApprovalCoordinator
return true;
}
return !MatchesAutoApprovedTool(autoApprovedToolNames, toolName, autoApprovedToolName);
return !MatchesAutoApprovedTool(autoApprovedToolNames, toolName, autoApprovedToolName)
&& !MatchesAutoApprovedToolName(autoApprovedToolNames, mcpServerApprovalKey);
}
internal static bool TryGetApprovalToolName(
@@ -327,6 +330,19 @@ internal sealed class CopilotApprovalCoordinator
return GetFallbackToolName(request);
}
private const string McpServerApprovalPrefix = "mcp_server:";
private static string? ResolveMcpServerApprovalKey(PermissionRequest request)
{
if (request is not PermissionRequestMcp mcp)
{
return null;
}
string? serverName = NormalizeOptionalString(mcp.ServerName);
return serverName is not null ? $"{McpServerApprovalPrefix}{serverName}" : null;
}
private static string? ResolveApprovalCacheKey(
string? toolName,
string? autoApprovedToolName)
@@ -843,6 +843,36 @@ public sealed class CopilotWorkflowRunnerTests
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "git.status"));
}
[Fact]
public void RequiresToolCallApproval_HonorsMcpServerLevelApprovalKey()
{
ApprovalPolicyDto policy = new()
{
Rules =
[
new ApprovalCheckpointRuleDto
{
Kind = "tool-call",
},
],
AutoApprovedToolNames = ["mcp_server:Git MCP"],
};
// Server-level key approves any tool from that server
Assert.False(CopilotApprovalCoordinator.RequiresToolCallApproval(
policy, "agent-1", "git.status", null, "mcp_server:Git MCP"));
Assert.False(CopilotApprovalCoordinator.RequiresToolCallApproval(
policy, "agent-1", "git.diff", null, "mcp_server:Git MCP"));
// Different server still requires approval
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(
policy, "agent-1", "fs.read", null, "mcp_server:Filesystem"));
// Non-MCP tools unaffected
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(
policy, "agent-1", "unknown_tool"));
}
[Fact]
public void TryGetApprovalToolName_ResolvesDirectNamesAndRuntimeFallbacks()
{