From aaa1f031a031c1cc21ae59d07436947f64be064a Mon Sep 17 00:00:00 2001 From: mthines Date: Sat, 17 Oct 2026 13:35:31 +0200 Subject: [PATCH 1/2] fix(web): derive MCP URLs for any Supabase URL, not only *.supabase.co --- packages/web/src/lib/mcp-url.spec.ts | 39 ++++++++++++++++++++++++---- packages/web/src/lib/mcp-url.ts | 33 +++++++++++++++++------ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/web/src/lib/mcp-url.spec.ts b/packages/web/src/lib/mcp-url.spec.ts index 189103914..b2cdbdfa0 100644 --- a/packages/web/src/lib/mcp-url.spec.ts +++ b/packages/web/src/lib/mcp-url.spec.ts @@ -42,10 +42,39 @@ describe('resolveMcpUrls', () => { expect(resolveMcpUrls().mcpUrl).toBe(FALLBACK); }); - it('strips both the https:// prefix and the .supabase.co suffix to isolate the ref', () => { - // A bare ref (no scheme, no suffix) leaves nothing to strip, so it is used - // verbatim as the project ref — proving the derivation is a plain replace. - process.env[KEY] = 'myref'; - expect(resolveMcpUrls().mcpUrl).toBe('https://myref.supabase.co/functions/v1/mcp'); + it('ignores a trailing slash on a *.supabase.co URL', () => { + process.env[KEY] = 'https://abcdefghijklmnop.supabase.co/'; + expect(resolveMcpUrls().mcpUrl).toBe('https://abcdefghijklmnop.supabase.co/functions/v1/mcp'); + }); + + it('derives the MCP URL from a self-hosted custom domain', () => { + process.env[KEY] = 'https://db.example.com'; + const { mcpUrl, webhookUrl } = resolveMcpUrls(); + expect(mcpUrl).toBe('https://db.example.com/functions/v1/mcp'); + expect(webhookUrl).toBe('https://db.example.com/functions/v1/mcp/webhooks/github'); + }); + + it('ignores a trailing slash on a custom domain', () => { + process.env[KEY] = 'https://db.example.com/'; + expect(resolveMcpUrls().mcpUrl).toBe('https://db.example.com/functions/v1/mcp'); + }); + + it('keeps the scheme and port of a local http Supabase', () => { + process.env[KEY] = 'http://127.0.0.1:54321'; + expect(resolveMcpUrls().mcpUrl).toBe('http://127.0.0.1:54321/functions/v1/mcp'); + }); + + it('keeps a reverse-proxy base path, minus its trailing slash', () => { + process.env[KEY] = 'https://example.com/supabase/'; + expect(resolveMcpUrls().mcpUrl).toBe('https://example.com/supabase/functions/v1/mcp'); + }); + + it.each([ + ['a bare word that is not a URL', 'myref'], + ['whitespace only', ' '], + ['a non-http scheme', 'ftp://db.example.com'], + ])('falls back to the hosted URL for %s', (_label, value) => { + process.env[KEY] = value; + expect(resolveMcpUrls().mcpUrl).toBe(FALLBACK); }); }); diff --git a/packages/web/src/lib/mcp-url.ts b/packages/web/src/lib/mcp-url.ts index 85402fa60..9a9b7eac8 100644 --- a/packages/web/src/lib/mcp-url.ts +++ b/packages/web/src/lib/mcp-url.ts @@ -1,13 +1,30 @@ +/** The hosted LoreKit MCP endpoint, used when no usable Supabase URL is configured. */ +const HOSTED_MCP_URL = 'https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/mcp'; + /** - * Resolve the user's MCP + webhook URLs from the Supabase project ref. - * Shared by the dashboard onboarding and the settings pages so the derivation - * lives in exactly one place. + * Normalise a Supabase base URL (`https://.supabase.co`, a self-hosted + * custom domain, or a local `http://127.0.0.1:54321`) to `origin + path` with + * no trailing slash. Returns `null` for anything that is not an absolute + * http(s) URL, so the caller can fall back instead of emitting a broken URL. + */ +function supabaseBaseUrl(raw: string): string | null { + let url: URL; + try { + url = new URL(raw.trim()); + } catch { + return null; + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') return null; + return `${url.origin}${url.pathname.replace(/\/+$/, '')}`; +} + +/** + * Resolve the user's MCP + webhook URLs from NEXT_PUBLIC_SUPABASE_URL — any + * Supabase base URL, not only `*.supabase.co`. Shared by the dashboard + * onboarding and the settings pages so the derivation lives in exactly one place. */ export function resolveMcpUrls(): { mcpUrl: string; webhookUrl: string } { - const supabaseUrl = process.env['NEXT_PUBLIC_SUPABASE_URL'] ?? ''; - const projectRef = supabaseUrl.replace('https://', '').replace('.supabase.co', ''); - const mcpUrl = projectRef - ? `https://${projectRef}.supabase.co/functions/v1/mcp` - : 'https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/mcp'; + const base = supabaseBaseUrl(process.env['NEXT_PUBLIC_SUPABASE_URL'] ?? ''); + const mcpUrl = base ? `${base}/functions/v1/mcp` : HOSTED_MCP_URL; return { mcpUrl, webhookUrl: `${mcpUrl}/webhooks/github` }; } From 4a0787778437d776a2b0b661ffcfa3835cf5df53 Mon Sep 17 00:00:00 2001 From: mthines Date: Sat, 17 Oct 2026 13:40:36 +0200 Subject: [PATCH 2/2] fix(web): resolve MCP URLs through supabaseUrl() so serve-local points at the local stack --- packages/web/src/lib/mcp-url.spec.ts | 10 ++++++++++ packages/web/src/lib/mcp-url.ts | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/web/src/lib/mcp-url.spec.ts b/packages/web/src/lib/mcp-url.spec.ts index b2cdbdfa0..d8edfbaf1 100644 --- a/packages/web/src/lib/mcp-url.spec.ts +++ b/packages/web/src/lib/mcp-url.spec.ts @@ -5,11 +5,15 @@ import { resolveMcpUrls } from './mcp-url'; // restore it around every case so the tests don't leak env state into each // other (or into the rest of the suite). const KEY = 'NEXT_PUBLIC_SUPABASE_URL'; +const LOCAL_FLAG = 'NEXT_PUBLIC_USE_LOCAL_SUPABASE'; const original = process.env[KEY]; +const originalFlag = process.env[LOCAL_FLAG]; afterEach(() => { if (original === undefined) delete process.env[KEY]; else process.env[KEY] = original; + if (originalFlag === undefined) delete process.env[LOCAL_FLAG]; + else process.env[LOCAL_FLAG] = originalFlag; }); // The hardcoded fallback ref baked into mcp-url.ts, used when the env var is @@ -77,4 +81,10 @@ describe('resolveMcpUrls', () => { process.env[KEY] = value; expect(resolveMcpUrls().mcpUrl).toBe(FALLBACK); }); + + it('follows the local stack when NEXT_PUBLIC_USE_LOCAL_SUPABASE is set, like every other client', () => { + process.env[KEY] = 'https://abcdefghijklmnop.supabase.co'; + process.env[LOCAL_FLAG] = 'true'; + expect(resolveMcpUrls().mcpUrl).toBe('http://127.0.0.1:54321/functions/v1/mcp'); + }); }); diff --git a/packages/web/src/lib/mcp-url.ts b/packages/web/src/lib/mcp-url.ts index 9a9b7eac8..2be8e72ec 100644 --- a/packages/web/src/lib/mcp-url.ts +++ b/packages/web/src/lib/mcp-url.ts @@ -1,3 +1,5 @@ +import { supabaseUrl } from './supabase/config'; + /** The hosted LoreKit MCP endpoint, used when no usable Supabase URL is configured. */ const HOSTED_MCP_URL = 'https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/mcp'; @@ -19,12 +21,13 @@ function supabaseBaseUrl(raw: string): string | null { } /** - * Resolve the user's MCP + webhook URLs from NEXT_PUBLIC_SUPABASE_URL — any + * Resolve the user's MCP + webhook URLs from the dashboard's Supabase URL + * (`supabaseUrl()`, so `NEXT_PUBLIC_USE_LOCAL_SUPABASE` is honoured) — any * Supabase base URL, not only `*.supabase.co`. Shared by the dashboard * onboarding and the settings pages so the derivation lives in exactly one place. */ export function resolveMcpUrls(): { mcpUrl: string; webhookUrl: string } { - const base = supabaseBaseUrl(process.env['NEXT_PUBLIC_SUPABASE_URL'] ?? ''); + const base = supabaseBaseUrl(supabaseUrl()); const mcpUrl = base ? `${base}/functions/v1/mcp` : HOSTED_MCP_URL; return { mcpUrl, webhookUrl: `${mcpUrl}/webhooks/github` }; }