Files
aryx/src/main/sessionToolingConfig.ts
T
David KayaandCopilot ac48aa58e0 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>
2026-03-27 19:15:37 +01:00

120 lines
3.4 KiB
TypeScript

import type {
RunTurnLspProfileConfig,
RunTurnMcpServerConfig,
RunTurnToolingConfig,
} from '@shared/contracts/sidecar';
import type {
LspProfileDefinition,
McpServerDefinition,
SessionToolingSelection,
WorkspaceToolingSettings,
} from '@shared/domain/tooling';
export function validateSessionToolingSelectionIds(
tooling: WorkspaceToolingSettings,
selection: SessionToolingSelection,
): void {
const knownMcpServerIds = new Set(tooling.mcpServers.map((server) => server.id));
const unknownMcpServerIds = selection.enabledMcpServerIds.filter((id) => !knownMcpServerIds.has(id));
if (unknownMcpServerIds.length > 0) {
throw new Error(`Unknown MCP server "${unknownMcpServerIds[0]}".`);
}
const knownLspProfileIds = new Set(tooling.lspProfiles.map((profile) => profile.id));
const unknownLspProfileIds = selection.enabledLspProfileIds.filter((id) => !knownLspProfileIds.has(id));
if (unknownLspProfileIds.length > 0) {
throw new Error(`Unknown LSP profile "${unknownLspProfileIds[0]}".`);
}
}
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]),
);
const lspProfilesById = new Map<string, LspProfileDefinition>(
tooling.lspProfiles.map((profile) => [profile.id, profile]),
);
const mcpServers = selection.enabledMcpServerIds.flatMap((id): RunTurnMcpServerConfig[] => {
const server = mcpServersById.get(id);
if (!server) {
return [];
}
if (server.transport === 'local') {
return [
{
id: server.id,
name: server.name,
transport: 'local',
tools: [...server.tools],
timeoutMs: server.timeoutMs,
command: server.command,
args: [...server.args],
cwd: server.cwd,
env: server.env ? { ...server.env } : undefined,
},
];
}
return [
{
id: server.id,
name: server.name,
transport: server.transport,
tools: [...server.tools],
timeoutMs: server.timeoutMs,
url: server.url,
headers: mergeAuthorizationHeader(server.url, server.headers, tokenLookup),
},
];
});
const lspProfiles = selection.enabledLspProfileIds.flatMap((id): RunTurnLspProfileConfig[] => {
const profile = lspProfilesById.get(id);
if (!profile) {
return [];
}
return [
{
id: profile.id,
name: profile.name,
command: profile.command,
args: [...profile.args],
languageId: profile.languageId,
fileExtensions: [...profile.fileExtensions],
},
];
});
if (mcpServers.length === 0 && lspProfiles.length === 0) {
return undefined;
}
return {
mcpServers,
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}`,
};
}