From 216b17b2accb4cdba9035151757882c18a8b15c7 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sat, 28 Mar 2026 19:53:25 +0100 Subject: [PATCH] fix: add origin-only well-known URL fallback for OAuth discovery Some MCP servers (e.g., eschat.microsoft.com/mcp) serve their OAuth Protected Resource Metadata at the origin without the path suffix. Add a third candidate URL that strips the path, matching VS Code's discovery behavior. Tried in order: 1. RFC 9728: {origin}/.well-known/{suffix}{path} 2. Appended: {origin}{path}/.well-known/{suffix} 3. Origin-only: {origin}/.well-known/{suffix} Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/services/mcpOAuthService.ts | 15 +++++++++++++-- src/main/services/mcpTokenStore.ts | 5 +++++ tests/main/mcpOAuthDiscovery.test.ts | 24 +++++++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/services/mcpOAuthService.ts b/src/main/services/mcpOAuthService.ts index 50957c7..bb66a6b 100644 --- a/src/main/services/mcpOAuthService.ts +++ b/src/main/services/mcpOAuthService.ts @@ -5,7 +5,7 @@ import electron from 'electron'; import type { McpOauthStaticClientConfig } from '@shared/domain/mcpAuth'; -import { storeToken, buildWellKnownUrl, buildWellKnownUrlFallback, type McpOAuthToken } from './mcpTokenStore'; +import { storeToken, buildWellKnownUrl, buildWellKnownUrlFallback, buildWellKnownUrlOriginOnly, type McpOAuthToken } from './mcpTokenStore'; const { shell } = electron; @@ -228,7 +228,18 @@ interface AuthServerMetadata { async function fetchWellKnownMetadata(baseUrl: string, suffix: string): Promise | undefined> { const rfcUrl = buildWellKnownUrl(baseUrl, suffix); const fallbackUrl = buildWellKnownUrlFallback(baseUrl, suffix); - const urls = rfcUrl === fallbackUrl ? [rfcUrl] : [rfcUrl, fallbackUrl]; + const originOnlyUrl = buildWellKnownUrlOriginOnly(baseUrl, suffix); + + // Deduplicate: RFC path, appended fallback, then origin-only (for servers + // that serve metadata at the origin without the resource path suffix). + const seen = new Set(); + const urls: string[] = []; + for (const url of [rfcUrl, fallbackUrl, originOnlyUrl]) { + if (!seen.has(url)) { + seen.add(url); + urls.push(url); + } + } for (const url of urls) { try { diff --git a/src/main/services/mcpTokenStore.ts b/src/main/services/mcpTokenStore.ts index e92a1ae..dedaa70 100644 --- a/src/main/services/mcpTokenStore.ts +++ b/src/main/services/mcpTokenStore.ts @@ -66,3 +66,8 @@ export function buildWellKnownUrlFallback(baseUrl: string, wellKnownSuffix: stri const base = baseUrl.replace(/\/+$/, ''); return `${base}/.well-known/${wellKnownSuffix}`; } + +export function buildWellKnownUrlOriginOnly(baseUrl: string, wellKnownSuffix: string): string { + const parsed = new URL(baseUrl); + return `${parsed.origin}/.well-known/${wellKnownSuffix}`; +} diff --git a/tests/main/mcpOAuthDiscovery.test.ts b/tests/main/mcpOAuthDiscovery.test.ts index 4a8f55d..83f5464 100644 --- a/tests/main/mcpOAuthDiscovery.test.ts +++ b/tests/main/mcpOAuthDiscovery.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test'; -import { buildWellKnownUrl, buildWellKnownUrlFallback } from '@main/services/mcpTokenStore'; +import { buildWellKnownUrl, buildWellKnownUrlFallback, buildWellKnownUrlOriginOnly } from '@main/services/mcpTokenStore'; describe('buildWellKnownUrl', () => { test('inserts well-known segment after origin for URL with path', () => { @@ -50,3 +50,25 @@ describe('buildWellKnownUrlFallback', () => { .toBe('https://auth.example.com/.well-known/oauth-authorization-server'); }); }); + +describe('buildWellKnownUrlOriginOnly', () => { + test('strips path and returns origin-only well-known URL', () => { + expect(buildWellKnownUrlOriginOnly('https://eschat.microsoft.com/mcp', 'oauth-protected-resource')) + .toBe('https://eschat.microsoft.com/.well-known/oauth-protected-resource'); + }); + + test('handles multi-level path', () => { + expect(buildWellKnownUrlOriginOnly('https://example.com/v1/mcp/api', 'oauth-protected-resource')) + .toBe('https://example.com/.well-known/oauth-protected-resource'); + }); + + test('matches RFC URL when base has no path', () => { + expect(buildWellKnownUrlOriginOnly('https://auth.example.com/', 'oauth-authorization-server')) + .toBe('https://auth.example.com/.well-known/oauth-authorization-server'); + }); + + test('handles URL with port', () => { + expect(buildWellKnownUrlOriginOnly('https://mcp.example.com:8443/v1/', 'oauth-protected-resource')) + .toBe('https://mcp.example.com:8443/.well-known/oauth-protected-resource'); + }); +});