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:
David Kaya
2026-03-27 19:15:37 +01:00
co-authored by Copilot
parent f9757d5ce2
commit ac48aa58e0
13 changed files with 635 additions and 8 deletions
+18 -1
View File
@@ -30,6 +30,7 @@ export function validateSessionToolingSelectionIds(
export function buildRunTurnToolingConfig(
tooling: WorkspaceToolingSettings,
selection: SessionToolingSelection,
tokenLookup?: (serverUrl: string) => string | undefined,
): RunTurnToolingConfig | undefined {
const mcpServersById = new Map<string, McpServerDefinition>(
tooling.mcpServers.map((server) => [server.id, server]),
@@ -68,7 +69,7 @@ export function buildRunTurnToolingConfig(
tools: [...server.tools],
timeoutMs: server.timeoutMs,
url: server.url,
headers: server.headers ? { ...server.headers } : undefined,
headers: mergeAuthorizationHeader(server.url, server.headers, tokenLookup),
},
];
});
@@ -100,3 +101,19 @@ export function buildRunTurnToolingConfig(
lspProfiles,
};
}
function mergeAuthorizationHeader(
serverUrl: string,
configHeaders: Record<string, string> | undefined,
tokenLookup: ((serverUrl: string) => string | undefined) | undefined,
): Record<string, string> | undefined {
const bearerToken = tokenLookup?.(serverUrl);
if (!bearerToken) {
return configHeaders ? { ...configHeaders } : undefined;
}
return {
...(configHeaders ?? {}),
Authorization: `Bearer ${bearerToken}`,
};
}