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'); + }); +});