Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions packages/web/src/lib/mcp-url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,10 +46,45 @@ 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);
});

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');
});
});
36 changes: 28 additions & 8 deletions packages/web/src/lib/mcp-url.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
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';

/**
* 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://<ref>.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 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 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(supabaseUrl());
const mcpUrl = base ? `${base}/functions/v1/mcp` : HOSTED_MCP_URL;
return { mcpUrl, webhookUrl: `${mcpUrl}/webhooks/github` };
}
Loading