mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-26 22:48:38 +02:00
- 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>
120 lines
3.4 KiB
TypeScript
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}`,
|
|
};
|
|
}
|