From 192c28f721a25a6096b0cceb88c19c7fab3df281 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Fri, 27 Mar 2026 19:01:46 +0100 Subject: [PATCH] feat: surface MCP OAuth sidecar events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/ProtocolModels.cs | 17 ++++ .../Services/CopilotMcpOAuthCoordinator.cs | 58 +++++++++++++ .../Services/CopilotTurnExecutionState.cs | 21 +++++ .../Services/CopilotWorkflowRunner.cs | 21 +++++ .../Services/ITurnWorkflowRunner.cs | 1 + .../Services/SidecarProtocolHost.cs | 1 + .../CopilotMcpOAuthCoordinatorTests.cs | 71 ++++++++++++++++ .../CopilotTurnExecutionStateTests.cs | 82 +++++++++++++++++++ .../SidecarProtocolHostTests.cs | 81 +++++++++++++++--- 9 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 sidecar/src/Aryx.AgentHost/Services/CopilotMcpOAuthCoordinator.cs create mode 100644 sidecar/tests/Aryx.AgentHost.Tests/CopilotMcpOAuthCoordinatorTests.cs create mode 100644 sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs diff --git a/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs b/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs index 9c3947c..3bffa47 100644 --- a/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs +++ b/sidecar/src/Aryx.AgentHost/Contracts/ProtocolModels.cs @@ -309,6 +309,23 @@ public sealed class UserInputRequestedEventDto : SidecarEventDto public bool? AllowFreeform { get; init; } } +public sealed class McpOauthStaticClientConfigDto +{ + public string ClientId { get; init; } = string.Empty; + public bool? PublicClient { get; init; } +} + +public sealed class McpOauthRequiredEventDto : SidecarEventDto +{ + public string SessionId { get; init; } = string.Empty; + public string OauthRequestId { get; init; } = string.Empty; + public string? AgentId { get; init; } + public string? AgentName { get; init; } + public string ServerName { get; init; } = string.Empty; + public string ServerUrl { get; init; } = string.Empty; + public McpOauthStaticClientConfigDto? StaticClientConfig { get; init; } +} + public sealed class ExitPlanModeRequestedEventDto : SidecarEventDto { public string SessionId { get; init; } = string.Empty; diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotMcpOAuthCoordinator.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotMcpOAuthCoordinator.cs new file mode 100644 index 0000000..8e32ce8 --- /dev/null +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotMcpOAuthCoordinator.cs @@ -0,0 +1,58 @@ +using Aryx.AgentHost.Contracts; +using GitHub.Copilot.SDK; + +namespace Aryx.AgentHost.Services; + +internal sealed class CopilotMcpOAuthCoordinator +{ + public McpOauthRequiredEventDto BuildMcpOauthRequiredEvent( + RunTurnCommandDto command, + PatternAgentDefinitionDto agent, + McpOauthRequiredEvent request) + { + ArgumentNullException.ThrowIfNull(command); + ArgumentNullException.ThrowIfNull(agent); + ArgumentNullException.ThrowIfNull(request); + + McpOauthRequiredData requestData = request.Data + ?? throw new InvalidOperationException("MCP OAuth request data is required."); + + string oauthRequestId = NormalizeOptionalString(requestData.RequestId) + ?? throw new InvalidOperationException("MCP OAuth request ID is required."); + string? normalizedAgentId = NormalizeOptionalString(agent.Id); + string? normalizedAgentName = NormalizeOptionalString(agent.Name) ?? normalizedAgentId; + + return new McpOauthRequiredEventDto + { + Type = "mcp-oauth-required", + RequestId = command.RequestId, + SessionId = command.SessionId, + OauthRequestId = oauthRequestId, + AgentId = normalizedAgentId, + AgentName = normalizedAgentName, + ServerName = NormalizeOptionalString(requestData.ServerName) ?? string.Empty, + ServerUrl = NormalizeOptionalString(requestData.ServerUrl) ?? string.Empty, + StaticClientConfig = BuildStaticClientConfig(requestData.StaticClientConfig), + }; + } + + private static McpOauthStaticClientConfigDto? BuildStaticClientConfig( + McpOauthRequiredDataStaticClientConfig? staticClientConfig) + { + if (staticClientConfig is null) + { + return null; + } + + return new McpOauthStaticClientConfigDto + { + ClientId = NormalizeOptionalString(staticClientConfig.ClientId) ?? string.Empty, + PublicClient = staticClientConfig.PublicClient, + }; + } + + private static string? NormalizeOptionalString(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } +} diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs index 2fb98d3..5f834c2 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs @@ -9,6 +9,7 @@ internal sealed class CopilotTurnExecutionState { private readonly RunTurnCommandDto _command; private readonly HashSet _startedAgents = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentQueue _pendingMcpOauthRequests = new(); private readonly ConcurrentDictionary _observedAgentsByMessageId = new(StringComparer.Ordinal); private readonly StreamingTranscriptBuffer _transcriptBuffer = new(); private int _fallbackMessageIndex; @@ -76,6 +77,9 @@ internal sealed class CopilotTurnExecutionState case AssistantReasoningDeltaEvent: ActiveAgent = agent; break; + case McpOauthRequiredEvent: + ActiveAgent = agent; + break; case ExitPlanModeRequestedEvent: HasPendingExitPlanModeRequest = true; ActiveAgent = agent; @@ -83,6 +87,23 @@ internal sealed class CopilotTurnExecutionState } } + public void EnqueuePendingMcpOauthRequest(McpOauthRequiredEventDto request) + { + ArgumentNullException.ThrowIfNull(request); + _pendingMcpOauthRequests.Enqueue(request); + } + + public IReadOnlyList DrainPendingMcpOauthRequests() + { + List pending = []; + while (_pendingMcpOauthRequests.TryDequeue(out McpOauthRequiredEventDto? request)) + { + pending.Add(request); + } + + return pending; + } + public bool TryResolveObservedAgentForMessage(string? messageId, out AgentIdentity agent) { agent = default; diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs index b54011c..1b91bb9 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotWorkflowRunner.cs @@ -10,6 +10,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner private readonly PatternValidator _patternValidator; private readonly CopilotApprovalCoordinator _approvalCoordinator = new(); private readonly CopilotUserInputCoordinator _userInputCoordinator = new(); + private readonly CopilotMcpOAuthCoordinator _mcpOAuthCoordinator = new(); private readonly CopilotExitPlanModeCoordinator _exitPlanModeCoordinator = new(); public CopilotWorkflowRunner(PatternValidator patternValidator) @@ -23,6 +24,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner Func onActivity, Func onApproval, Func onUserInput, + Func onMcpOAuthRequired, Func onExitPlanMode, CancellationToken cancellationToken) { @@ -58,6 +60,12 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner (agent, sessionEvent) => { state.ObserveSessionEvent(agent, sessionEvent); + if (sessionEvent is McpOauthRequiredEvent mcpOauthRequired) + { + state.EnqueuePendingMcpOauthRequest( + _mcpOAuthCoordinator.BuildMcpOauthRequiredEvent(command, agent, mcpOauthRequired)); + } + if (sessionEvent is ExitPlanModeRequestedEvent exitPlanModeRequested) { _exitPlanModeCoordinator.RecordExitPlanModeRequest(command, agent, exitPlanModeRequested); @@ -75,16 +83,19 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner { bool shouldEndTurn = await HandleWorkflowEventAsync(command, evt, inputMessages, state, onDelta, onActivity) .ConfigureAwait(false); + await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false); if (shouldEndTurn) { break; } } + await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false); return state.FinalizeCompletedMessages(); } catch (OperationCanceledException) when (runCancellation.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { + await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false); ExitPlanModeRequestedEventDto? exitPlanModeEvent = _exitPlanModeCoordinator.ConsumePendingRequest(command.RequestId); if (exitPlanModeEvent is null || !state.HasPendingExitPlanModeRequest) @@ -97,6 +108,16 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner } } + private static async Task EmitPendingMcpOauthRequestsAsync( + CopilotTurnExecutionState state, + Func onMcpOAuthRequired) + { + foreach (McpOauthRequiredEventDto request in state.DrainPendingMcpOauthRequests()) + { + await onMcpOAuthRequired(request).ConfigureAwait(false); + } + } + public Task ResolveApprovalAsync( ResolveApprovalCommandDto command, CancellationToken cancellationToken) diff --git a/sidecar/src/Aryx.AgentHost/Services/ITurnWorkflowRunner.cs b/sidecar/src/Aryx.AgentHost/Services/ITurnWorkflowRunner.cs index 800ba7e..a8d71f5 100644 --- a/sidecar/src/Aryx.AgentHost/Services/ITurnWorkflowRunner.cs +++ b/sidecar/src/Aryx.AgentHost/Services/ITurnWorkflowRunner.cs @@ -10,6 +10,7 @@ public interface ITurnWorkflowRunner Func onActivity, Func onApproval, Func onUserInput, + Func onMcpOAuthRequired, Func onExitPlanMode, CancellationToken cancellationToken); diff --git a/sidecar/src/Aryx.AgentHost/Services/SidecarProtocolHost.cs b/sidecar/src/Aryx.AgentHost/Services/SidecarProtocolHost.cs index 8771fc3..54a2d04 100644 --- a/sidecar/src/Aryx.AgentHost/Services/SidecarProtocolHost.cs +++ b/sidecar/src/Aryx.AgentHost/Services/SidecarProtocolHost.cs @@ -188,6 +188,7 @@ public sealed class SidecarProtocolHost activity => WriteAsync(context.Output, activity, turnCancellation.Token), approval => WriteAsync(context.Output, approval, turnCancellation.Token), userInput => WriteAsync(context.Output, userInput, turnCancellation.Token), + mcpOauth => WriteAsync(context.Output, mcpOauth, turnCancellation.Token), exitPlanMode => WriteAsync(context.Output, exitPlanMode, turnCancellation.Token), turnCancellation.Token) .ConfigureAwait(false); diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotMcpOAuthCoordinatorTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotMcpOAuthCoordinatorTests.cs new file mode 100644 index 0000000..adbbff6 --- /dev/null +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotMcpOAuthCoordinatorTests.cs @@ -0,0 +1,71 @@ +using Aryx.AgentHost.Contracts; +using Aryx.AgentHost.Services; +using GitHub.Copilot.SDK; + +namespace Aryx.AgentHost.Tests; + +public sealed class CopilotMcpOAuthCoordinatorTests +{ + [Fact] + public void BuildMcpOauthRequiredEvent_MapsSdkEventToProtocolEvent() + { + CopilotMcpOAuthCoordinator coordinator = new(); + RunTurnCommandDto command = CreateCommand(); + + McpOauthRequiredEventDto oauthEvent = coordinator.BuildMcpOauthRequiredEvent( + command, + command.Pattern.Agents[0], + new McpOauthRequiredEvent + { + Data = new McpOauthRequiredData + { + RequestId = " oauth-request-1 ", + ServerName = " Example MCP ", + ServerUrl = " https://example.com/mcp ", + StaticClientConfig = new McpOauthRequiredDataStaticClientConfig + { + ClientId = " aryx-client ", + PublicClient = true, + }, + }, + }); + + Assert.Equal("mcp-oauth-required", oauthEvent.Type); + Assert.Equal("turn-1", oauthEvent.RequestId); + Assert.Equal("session-1", oauthEvent.SessionId); + Assert.Equal("oauth-request-1", oauthEvent.OauthRequestId); + Assert.Equal("agent-1", oauthEvent.AgentId); + Assert.Equal("Primary", oauthEvent.AgentName); + Assert.Equal("Example MCP", oauthEvent.ServerName); + Assert.Equal("https://example.com/mcp", oauthEvent.ServerUrl); + Assert.NotNull(oauthEvent.StaticClientConfig); + Assert.Equal("aryx-client", oauthEvent.StaticClientConfig!.ClientId); + Assert.True(oauthEvent.StaticClientConfig.PublicClient); + } + + private static RunTurnCommandDto CreateCommand() + { + return new RunTurnCommandDto + { + RequestId = "turn-1", + SessionId = "session-1", + Pattern = new PatternDefinitionDto + { + Id = "pattern-1", + Name = "MCP OAuth Pattern", + Mode = "single", + Availability = "available", + Agents = + [ + new PatternAgentDefinitionDto + { + Id = "agent-1", + Name = "Primary", + Model = "gpt-5.4", + Instructions = "Help with the request.", + }, + ], + }, + }; + } +} diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs new file mode 100644 index 0000000..50d13e2 --- /dev/null +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs @@ -0,0 +1,82 @@ +using Aryx.AgentHost.Contracts; +using Aryx.AgentHost.Services; +using GitHub.Copilot.SDK; + +namespace Aryx.AgentHost.Tests; + +public sealed class CopilotTurnExecutionStateTests +{ + [Fact] + public void ObserveSessionEvent_McpOauthRequired_SetsActiveAgent() + { + RunTurnCommandDto command = CreateCommand(); + CopilotTurnExecutionState state = new(command); + + state.ObserveSessionEvent( + command.Pattern.Agents[0], + new McpOauthRequiredEvent + { + Data = new McpOauthRequiredData + { + RequestId = "oauth-request-1", + ServerName = "Example MCP", + ServerUrl = "https://example.com/mcp", + }, + }); + + Assert.True(state.ActiveAgent.HasValue); + Assert.Equal("agent-1", state.ActiveAgent.Value.AgentId); + Assert.Equal("Primary", state.ActiveAgent.Value.AgentName); + } + + [Fact] + public void DrainPendingMcpOauthRequests_ReturnsQueuedRequestsAndClearsQueue() + { + RunTurnCommandDto command = CreateCommand(); + CopilotTurnExecutionState state = new(command); + McpOauthRequiredEventDto request = new() + { + Type = "mcp-oauth-required", + RequestId = command.RequestId, + SessionId = command.SessionId, + OauthRequestId = "oauth-request-1", + ServerName = "Example MCP", + ServerUrl = "https://example.com/mcp", + }; + + state.EnqueuePendingMcpOauthRequest(request); + + IReadOnlyList firstDrain = state.DrainPendingMcpOauthRequests(); + IReadOnlyList secondDrain = state.DrainPendingMcpOauthRequests(); + + McpOauthRequiredEventDto drained = Assert.Single(firstDrain); + Assert.Equal("oauth-request-1", drained.OauthRequestId); + Assert.Empty(secondDrain); + } + + private static RunTurnCommandDto CreateCommand() + { + return new RunTurnCommandDto + { + RequestId = "turn-1", + SessionId = "session-1", + Pattern = new PatternDefinitionDto + { + Id = "pattern-1", + Name = "MCP OAuth Pattern", + Mode = "single", + Availability = "available", + Agents = + [ + new PatternAgentDefinitionDto + { + Id = "agent-1", + Name = "Primary", + Model = "gpt-5.4", + Instructions = "Help with the request.", + }, + ], + }, + }; + } +} diff --git a/sidecar/tests/Aryx.AgentHost.Tests/SidecarProtocolHostTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/SidecarProtocolHostTests.cs index b8e84a3..bf0134c 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/SidecarProtocolHostTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/SidecarProtocolHostTests.cs @@ -113,7 +113,7 @@ public sealed class SidecarProtocolHostTests { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { await onActivity(new AgentActivityEventDto { @@ -238,7 +238,7 @@ public sealed class SidecarProtocolHostTests string? capturedMode = null; SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { capturedMode = command.Mode; return []; @@ -274,7 +274,7 @@ public sealed class SidecarProtocolHostTests { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { await onApproval(new ApprovalRequestedEventDto { @@ -352,7 +352,7 @@ public sealed class SidecarProtocolHostTests { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { await onUserInput(new UserInputRequestedEventDto { @@ -420,12 +420,70 @@ public sealed class SidecarProtocolHostTests }); } + [Fact] + public async Task RunTurnCommand_ReturnsMcpOauthRequiredEvents() + { + SidecarProtocolHost host = new( + new PatternValidator(), + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => + { + await onMcpOAuthRequired(new McpOauthRequiredEventDto + { + Type = "mcp-oauth-required", + RequestId = command.RequestId, + SessionId = command.SessionId, + OauthRequestId = "oauth-request-1", + AgentId = "agent-1", + AgentName = "Primary", + ServerName = "Example MCP", + ServerUrl = "https://example.com/mcp", + StaticClientConfig = new McpOauthStaticClientConfigDto + { + ClientId = "aryx-client", + PublicClient = true, + }, + }); + + return []; + })); + + IReadOnlyList events = await RunHostAsync( + CreateRunTurnCommand(requestId: "turn-mcp-oauth"), + host); + + Assert.Collection( + events, + oauthEvent => + { + Assert.Equal("mcp-oauth-required", oauthEvent.GetProperty("type").GetString()); + Assert.Equal("turn-mcp-oauth", oauthEvent.GetProperty("requestId").GetString()); + Assert.Equal("session-1", oauthEvent.GetProperty("sessionId").GetString()); + Assert.Equal("oauth-request-1", oauthEvent.GetProperty("oauthRequestId").GetString()); + Assert.Equal("Primary", oauthEvent.GetProperty("agentName").GetString()); + Assert.Equal("Example MCP", oauthEvent.GetProperty("serverName").GetString()); + Assert.Equal("https://example.com/mcp", oauthEvent.GetProperty("serverUrl").GetString()); + JsonElement staticClientConfig = oauthEvent.GetProperty("staticClientConfig"); + Assert.Equal("aryx-client", staticClientConfig.GetProperty("clientId").GetString()); + Assert.True(staticClientConfig.GetProperty("publicClient").GetBoolean()); + }, + completionEvent => + { + Assert.Equal("turn-complete", completionEvent.GetProperty("type").GetString()); + Assert.False(completionEvent.GetProperty("cancelled").GetBoolean()); + }, + commandCompleteEvent => + { + Assert.Equal("command-complete", commandCompleteEvent.GetProperty("type").GetString()); + Assert.Equal("turn-mcp-oauth", commandCompleteEvent.GetProperty("requestId").GetString()); + }); + } + [Fact] public async Task RunTurnCommand_ReturnsExitPlanModeEvents() { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { await onExitPlanMode(new ExitPlanModeRequestedEventDto { @@ -500,7 +558,7 @@ public sealed class SidecarProtocolHostTests { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => { await Task.Delay(Timeout.Infinite, cancellationToken); return []; @@ -548,7 +606,7 @@ public sealed class SidecarProtocolHostTests { SidecarProtocolHost host = new( new PatternValidator(), - new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => [])); + new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => [])); await RunHostAsync(CreateRunTurnCommand(requestId: "turn-completed"), host); @@ -571,7 +629,7 @@ public sealed class SidecarProtocolHostTests SidecarProtocolHost host = new( new PatternValidator(), new FakeWorkflowRunner( - handler: async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => [], + handler: async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => [], resolveApprovalHandler: (command, cancellationToken) => { captured = command; @@ -602,7 +660,7 @@ public sealed class SidecarProtocolHostTests SidecarProtocolHost host = new( new PatternValidator(), new FakeWorkflowRunner( - handler: async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => [], + handler: async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => [], resolveUserInputHandler: (command, cancellationToken) => { captured = command; @@ -892,6 +950,7 @@ public sealed class SidecarProtocolHostTests Func, Func, Func, + Func, Func, CancellationToken, Task>> _handler; @@ -905,6 +964,7 @@ public sealed class SidecarProtocolHostTests Func, Func, Func, + Func, Func, CancellationToken, Task>> handler, @@ -922,10 +982,11 @@ public sealed class SidecarProtocolHostTests Func onActivity, Func onApproval, Func onUserInput, + Func onMcpOAuthRequired, Func onExitPlanMode, CancellationToken cancellationToken) { - return _handler(command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken); + return _handler(command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken); } public Task ResolveApprovalAsync(