feat: proactive OAuth when enabling HTTP MCP servers

When a user enables an HTTP MCP server for a session, proactively probe
the server URL. If it returns 401 and has discoverable OAuth metadata
(RFC 9728), automatically trigger the full OAuth 2.1 + PKCE flow and
open the browser for consent — matching VS Code's behavior.

- Add requiresOAuth() probe: GET server URL → check 401 → check PRM
- Add probeAndAuthenticateHttpMcpServers() in AryxAppService
- Call proactive probe from updateSessionTooling (fire-and-forget)
- Skip servers that already have stored tokens

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-27 19:26:59 +01:00
co-authored by Copilot
parent ac48aa58e0
commit f0c2b4982b
3 changed files with 77 additions and 4 deletions
+48 -2
View File
@@ -86,6 +86,8 @@ import {
type AppearanceTheme,
type LspProfileDefinition,
type McpServerDefinition,
type SessionToolingSelection,
type WorkspaceToolingSettings,
normalizeLspProfileDefinition,
normalizeMcpServerDefinition,
normalizeSessionToolingSelection,
@@ -110,7 +112,7 @@ import {
validateSessionToolingSelectionIds,
} from '@main/sessionToolingConfig';
import { getStoredToken } from '@main/services/mcpTokenStore';
import { performMcpOAuthFlow } from '@main/services/mcpOAuthService';
import { performMcpOAuthFlow, requiresOAuth } from '@main/services/mcpOAuthService';
const { dialog, shell } = electron;
@@ -927,6 +929,41 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
return this.persistAndBroadcast(workspaceAfter);
}
/**
* Proactively probes newly-enabled HTTP MCP servers for OAuth requirements.
* If a server returns 401 and has discoverable OAuth metadata, automatically
* triggers the OAuth flow (opens browser for consent) without user interaction.
*/
private async probeAndAuthenticateHttpMcpServers(
sessionId: string,
tooling: WorkspaceToolingSettings,
selection: SessionToolingSelection,
): Promise<void> {
const httpServers = selection.enabledMcpServerIds
.map((id) => tooling.mcpServers.find((s) => s.id === id))
.filter((s): s is McpServerDefinition => !!s && s.transport !== 'local')
.filter((s) => s.transport === 'http' || s.transport === 'sse');
for (const server of httpServers) {
if (server.transport === 'local') continue;
const existingToken = getStoredToken(server.url);
if (existingToken) continue;
try {
const needsAuth = await requiresOAuth(server.url);
if (!needsAuth) continue;
const result = await performMcpOAuthFlow({ serverUrl: server.url });
if (!result.success) {
console.warn(`[aryx oauth] Proactive auth failed for ${server.name}: ${result.error}`);
}
} catch (err) {
console.warn(`[aryx oauth] Proactive auth probe failed for ${server.name}:`, err);
}
}
}
async updateSessionTooling(
sessionId: string,
enabledMcpServerIds: string[],
@@ -951,7 +988,16 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
session.tooling = selection;
session.updatedAt = nowIso();
return this.persistAndBroadcast(workspace);
const result = await this.persistAndBroadcast(workspace);
// Proactively authenticate HTTP MCP servers that need OAuth
void this.probeAndAuthenticateHttpMcpServers(
sessionId,
resolveProjectToolingSettings(workspace.settings, project.discoveredTooling),
selection,
);
return result;
}
async updateSessionApprovalSettings(
+26
View File
@@ -21,6 +21,32 @@ export interface McpOAuthFlowResult {
error?: string;
}
/**
* Probes an MCP server URL to determine if it requires OAuth authentication.
* Returns true if the server responds with 401 and has discoverable OAuth metadata.
*/
export async function requiresOAuth(serverUrl: string): Promise<boolean> {
try {
const response = await fetch(serverUrl, {
method: 'GET',
signal: AbortSignal.timeout(5_000),
});
if (response.status !== 401) {
return false;
}
const base = serverUrl.replace(/\/+$/, '');
const prmResponse = await fetch(`${base}/.well-known/oauth-protected-resource`, {
signal: AbortSignal.timeout(5_000),
});
return prmResponse.ok;
} catch {
return false;
}
}
/**
* Performs the full MCP OAuth 2.1 + PKCE flow:
* 1. Discover protected resource metadata (RFC 9728)
+3 -2
View File
@@ -5,6 +5,7 @@ import {
validateSessionToolingSelectionIds,
} from '@main/sessionToolingConfig';
import type { SessionToolingSelection, WorkspaceToolingSettings } from '@shared/domain/tooling';
import type { RunTurnRemoteMcpServerConfig } from '@shared/contracts/sidecar';
const TIMESTAMP = '2026-03-25T00:00:00.000Z';
@@ -157,7 +158,7 @@ describe('session tooling config helpers', () => {
() => 'my-token',
);
expect(config?.mcpServers[0].headers).toEqual({
expect((config?.mcpServers[0] as RunTurnRemoteMcpServerConfig).headers).toEqual({
'X-Custom': 'value',
Authorization: 'Bearer my-token',
});
@@ -170,6 +171,6 @@ describe('session tooling config helpers', () => {
() => undefined,
);
expect(config?.mcpServers[0].headers).toEqual({ Authorization: 'Bearer token' });
expect((config?.mcpServers[0] as RunTurnRemoteMcpServerConfig).headers).toEqual({ Authorization: 'Bearer token' });
});
});