feat: add MCP OAuth authentication frontend wiring and banner

- Add McpOauthRequiredEvent and McpOauthStaticClientConfigEvent to sidecar contracts
- Add PendingMcpAuthRecord domain type with status tracking (pending/authenticating/failed/done)
- Add pendingMcpAuth field to SessionRecord for session-level auth state
- Wire onMcpOAuthRequired callback through sidecarProcess, runTurnPending, and AryxAppService
- Handle mcp-oauth-required events: set session pendingMcpAuth, clear on turn finalize/cancel
- Add dismissSessionMcpAuth IPC channel with preload binding and handler registration
- Create McpAuthBanner component (amber-themed, shows server name/URL, dismiss button)
- Integrate McpAuthBanner into ChatPane with placeholder text and App.tsx callback
- Update test fixtures for new RunTurnPendingCommand.onMcpOAuthRequired field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-27 19:09:11 +01:00
co-authored by Copilot
parent 192c28f721
commit f9757d5ce2
14 changed files with 196 additions and 2 deletions
+42
View File
@@ -8,6 +8,7 @@ import type {
AgentActivityEvent,
ApprovalRequestedEvent,
ExitPlanModeRequestedEvent,
McpOauthRequiredEvent,
RunTurnToolingConfig,
SidecarCapabilities,
TurnDeltaEvent,
@@ -586,6 +587,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
session.status = 'running';
session.lastError = undefined;
session.pendingPlanReview = undefined;
session.pendingMcpAuth = undefined;
session.updatedAt = occurredAt;
session.runs = [
createSessionRunRecord({
@@ -634,6 +636,9 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
await this.handleUserInputRequested(workspace, session.id, requestId, event, (answer, wasFreeform) =>
this.sidecar.resolveUserInput(event.userInputId, answer, wasFreeform));
},
async (event) => {
await this.handleMcpOAuthRequired(workspace, session.id, event);
},
async (event) => {
await this.handleExitPlanModeRequested(workspace, session.id, event);
},
@@ -874,6 +879,16 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
return this.persistAndBroadcast(workspace);
}
async dismissSessionMcpAuth(sessionId: string): Promise<WorkspaceState> {
const workspace = await this.loadWorkspace();
const session = this.requireSession(workspace, sessionId);
session.pendingMcpAuth = undefined;
session.updatedAt = nowIso();
return this.persistAndBroadcast(workspace);
}
async updateSessionTooling(
sessionId: string,
enabledMcpServerIds: string[],
@@ -1210,6 +1225,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
session.lastError = undefined;
session.pendingUserInput = undefined;
session.pendingPlanReview = undefined;
session.pendingMcpAuth = undefined;
session.updatedAt = completedAt;
const completedRun = this.updateSessionRun(session, requestId, (run) =>
completeSessionRunRecord(run, completedAt));
@@ -1242,6 +1258,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
session.lastError = undefined;
session.pendingUserInput = undefined;
session.pendingPlanReview = undefined;
session.pendingMcpAuth = undefined;
session.updatedAt = cancelledAt;
const cancelledRun = this.updateSessionRun(session, requestId, (run) =>
cancelSessionRunRecord(run, cancelledAt));
@@ -1340,6 +1357,31 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
await this.persistAndBroadcast(workspace);
}
private async handleMcpOAuthRequired(
workspace: WorkspaceState,
sessionId: string,
event: McpOauthRequiredEvent,
): Promise<void> {
const session = this.requireSession(workspace, sessionId);
const requestedAt = nowIso();
session.pendingMcpAuth = {
id: event.oauthRequestId,
status: 'pending',
agentId: event.agentId,
agentName: event.agentName,
serverName: event.serverName,
serverUrl: event.serverUrl,
staticClientConfig: event.staticClientConfig
? { clientId: event.staticClientConfig.clientId, publicClient: event.staticClientConfig.publicClient }
: undefined,
requestedAt,
};
session.updatedAt = requestedAt;
await this.persistAndBroadcast(workspace);
}
private createPendingApprovalFromSidecarEvent(event: ApprovalRequestedEvent): PendingApprovalRecord {
return {
id: event.approvalId,