mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: implement MCP OAuth 2.1 flow, token storage, and injection
- Create mcpTokenStore: in-memory token storage keyed by normalized server URL with automatic expiry checking - Create mcpOAuthService: full OAuth 2.1 + PKCE flow implementation - Protected Resource Metadata discovery (RFC 9728) - Authorization Server Metadata fetch (RFC 8414) - Dynamic Client Registration (RFC 7591) when no static client ID - PKCE S256 code challenge generation - Local HTTP callback server for auth code receipt - Browser-based consent via Electron shell.openExternal - Authorization code to token exchange - Add startSessionMcpAuth IPC channel and handler to trigger OAuth flow - Inject stored OAuth tokens as Authorization headers in buildRunTurnToolingConfig - Update McpAuthBanner with 'Authenticate in browser' button and loading state - Add tests for token store (7 tests) and token injection (3 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -109,6 +109,8 @@ import {
|
||||
buildRunTurnToolingConfig as buildSessionToolingConfig,
|
||||
validateSessionToolingSelectionIds,
|
||||
} from '@main/sessionToolingConfig';
|
||||
import { getStoredToken } from '@main/services/mcpTokenStore';
|
||||
import { performMcpOAuthFlow } from '@main/services/mcpOAuthService';
|
||||
|
||||
const { dialog, shell } = electron;
|
||||
|
||||
@@ -889,6 +891,42 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async startSessionMcpAuth(sessionId: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
|
||||
if (!session.pendingMcpAuth) {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
session.pendingMcpAuth.status = 'authenticating';
|
||||
session.updatedAt = nowIso();
|
||||
await this.persistAndBroadcast(workspace);
|
||||
|
||||
const result = await performMcpOAuthFlow({
|
||||
serverUrl: session.pendingMcpAuth.serverUrl,
|
||||
staticClientConfig: session.pendingMcpAuth.staticClientConfig,
|
||||
});
|
||||
|
||||
const workspaceAfter = await this.loadWorkspace();
|
||||
const sessionAfter = this.requireSession(workspaceAfter, sessionId);
|
||||
|
||||
if (!sessionAfter.pendingMcpAuth) {
|
||||
return workspaceAfter;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
sessionAfter.pendingMcpAuth.status = 'authenticated';
|
||||
sessionAfter.pendingMcpAuth.completedAt = nowIso();
|
||||
} else {
|
||||
sessionAfter.pendingMcpAuth.status = 'failed';
|
||||
sessionAfter.pendingMcpAuth.errorMessage = result.error ?? 'Authentication failed';
|
||||
}
|
||||
|
||||
sessionAfter.updatedAt = nowIso();
|
||||
return this.persistAndBroadcast(workspaceAfter);
|
||||
}
|
||||
|
||||
async updateSessionTooling(
|
||||
sessionId: string,
|
||||
enabledMcpServerIds: string[],
|
||||
@@ -1600,7 +1638,10 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const tooling = resolveProjectToolingSettings(workspace.settings, project.discoveredTooling);
|
||||
const selection = resolveSessionToolingSelection(session);
|
||||
validateSessionToolingSelectionIds(tooling, selection);
|
||||
return buildSessionToolingConfig(tooling, selection);
|
||||
return buildSessionToolingConfig(tooling, selection, (serverUrl) => {
|
||||
const token = getStoredToken(serverUrl);
|
||||
return token?.accessToken;
|
||||
});
|
||||
}
|
||||
|
||||
private async syncUserDiscoveredTooling(workspace: WorkspaceState): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user