From ee1700ad06f5f8ea44a34dc8e0804404b5f3586c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 19 Jan 2026 21:19:39 -0500 Subject: [PATCH 1/4] Add logging to MCP auth verification for debugging Logs Privy JWT and API key validation attempts to help debug authentication failures in tool calls. Co-Authored-By: Claude Opus 4.5 --- lib/mcp/verifyApiKey.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mcp/verifyApiKey.ts b/lib/mcp/verifyApiKey.ts index 8cf3d91e..cba2ffb6 100644 --- a/lib/mcp/verifyApiKey.ts +++ b/lib/mcp/verifyApiKey.ts @@ -25,6 +25,7 @@ export async function verifyBearerToken( bearerToken?: string, ): Promise { if (!bearerToken) { + console.error("[MCP Auth] No bearer token provided"); return undefined; } @@ -32,6 +33,7 @@ export async function verifyBearerToken( try { const accountId = await getAccountIdByAuthToken(bearerToken); + console.log("[MCP Auth] Privy JWT validated, accountId:", accountId); return { token: bearerToken, scopes: ["mcp:tools"], @@ -41,8 +43,8 @@ export async function verifyBearerToken( orgId: null, }, }; - } catch { - // Privy validation failed, try API key + } catch (privyError) { + console.log("[MCP Auth] Privy JWT validation failed:", privyError instanceof Error ? privyError.message : privyError); } // Try API key validation @@ -50,9 +52,11 @@ export async function verifyBearerToken( const accountId = await getAccountIdByApiKey(bearerToken); if (!accountId) { + console.error("[MCP Auth] API key validation returned no accountId"); return undefined; } + console.log("[MCP Auth] API key validated, accountId:", accountId); return { token: bearerToken, scopes: ["mcp:tools"], @@ -62,7 +66,8 @@ export async function verifyBearerToken( orgId: null, }, }; - } catch { + } catch (apiKeyError) { + console.error("[MCP Auth] API key validation failed:", apiKeyError instanceof Error ? apiKeyError.message : apiKeyError); return undefined; } } From 832df658d85929405cfc6c87a6132d3af6a06311 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 19 Jan 2026 21:23:08 -0500 Subject: [PATCH 2/4] Revert "Add logging to MCP auth verification for debugging" This reverts commit ee1700ad06f5f8ea44a34dc8e0804404b5f3586c. --- lib/mcp/verifyApiKey.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/mcp/verifyApiKey.ts b/lib/mcp/verifyApiKey.ts index cba2ffb6..8cf3d91e 100644 --- a/lib/mcp/verifyApiKey.ts +++ b/lib/mcp/verifyApiKey.ts @@ -25,7 +25,6 @@ export async function verifyBearerToken( bearerToken?: string, ): Promise { if (!bearerToken) { - console.error("[MCP Auth] No bearer token provided"); return undefined; } @@ -33,7 +32,6 @@ export async function verifyBearerToken( try { const accountId = await getAccountIdByAuthToken(bearerToken); - console.log("[MCP Auth] Privy JWT validated, accountId:", accountId); return { token: bearerToken, scopes: ["mcp:tools"], @@ -43,8 +41,8 @@ export async function verifyBearerToken( orgId: null, }, }; - } catch (privyError) { - console.log("[MCP Auth] Privy JWT validation failed:", privyError instanceof Error ? privyError.message : privyError); + } catch { + // Privy validation failed, try API key } // Try API key validation @@ -52,11 +50,9 @@ export async function verifyBearerToken( const accountId = await getAccountIdByApiKey(bearerToken); if (!accountId) { - console.error("[MCP Auth] API key validation returned no accountId"); return undefined; } - console.log("[MCP Auth] API key validated, accountId:", accountId); return { token: bearerToken, scopes: ["mcp:tools"], @@ -66,8 +62,7 @@ export async function verifyBearerToken( orgId: null, }, }; - } catch (apiKeyError) { - console.error("[MCP Auth] API key validation failed:", apiKeyError instanceof Error ? apiKeyError.message : apiKeyError); + } catch { return undefined; } } From 3b9d4b7db28d21be4efca05267fd41f49bbc4ff9 Mon Sep 17 00:00:00 2001 From: "sweetman.eth" Date: Tue, 20 Jan 2026 13:42:41 -0300 Subject: [PATCH 3/4] fix: add CORS headers to streaming chat response (#138) - Add CORS headers to createUIMessageStreamResponse call in handleChatStream - Add x-api-key to Access-Control-Allow-Headers in getCorsHeaders - Update handleChatStream test to expect CORS headers Fixes cross-origin requests to /api/chat endpoint being blocked. Co-authored-by: Claude Opus 4.5 --- lib/chat/__tests__/handleChatStream.test.ts | 5 +++++ lib/chat/handleChatStream.ts | 2 +- lib/networking/getCorsHeaders.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index 5943feed..b29127dc 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -162,6 +162,11 @@ describe("handleChatStream", () => { expect(mockCreateUIMessageStream).toHaveBeenCalled(); expect(mockCreateUIMessageStreamResponse).toHaveBeenCalledWith({ stream: mockStream, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, x-api-key", + }, }); expect(result).toBe(mockResponse); }); diff --git a/lib/chat/handleChatStream.ts b/lib/chat/handleChatStream.ts index 56cfb963..fe971374 100644 --- a/lib/chat/handleChatStream.ts +++ b/lib/chat/handleChatStream.ts @@ -58,7 +58,7 @@ export async function handleChatStream(request: NextRequest): Promise }, }); - return createUIMessageStreamResponse({ stream }); + return createUIMessageStreamResponse({ stream, headers: getCorsHeaders() }); } catch (e) { console.error("/api/chat Global error:", e); return NextResponse.json( diff --git a/lib/networking/getCorsHeaders.ts b/lib/networking/getCorsHeaders.ts index 0a6c3a02..233b32df 100644 --- a/lib/networking/getCorsHeaders.ts +++ b/lib/networking/getCorsHeaders.ts @@ -7,6 +7,6 @@ export function getCorsHeaders(): Record { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH", - "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With", + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, x-api-key", }; } From 99ed986dfe5699101380e49faa851dc72b5ff1b8 Mon Sep 17 00:00:00 2001 From: "sweetman.eth" Date: Tue, 20 Jan 2026 15:24:20 -0300 Subject: [PATCH 4/4] refactor: Use HTTP transport for MCP to properly pass authInfo (#139) * refactor: Use HTTP transport for MCP to properly pass authInfo Replace InMemoryTransport with StreamableHTTPClientTransport to make HTTP requests to /api/mcp endpoint with forwarded auth token. This ensures MCP tool handlers receive proper authInfo through the established withMcpAuth middleware flow. - Add authToken to ChatRequestBody type - Extract auth token from request headers in validateChatRequest - Use HTTP transport to /api/mcp with Authorization header - Update tests to include authToken in request bodies Co-Authored-By: Claude Opus 4.5 * fix: Make authToken optional for internal flows Internal flows like email processing don't have an auth token from HTTP headers. Make authToken optional and skip MCP tools when not present. Co-Authored-By: Claude Opus 4.5 * refactor: Extract getBaseUrl to lib/networking Move getBaseUrl utility to its own file following SRP. Co-Authored-By: Claude Opus 4.5 * refactor: Extract getMcpTools to lib/mcp Move MCP tools fetching logic to its own file following SRP. Co-Authored-By: Claude Opus 4.5 * test: Add unit tests for getBaseUrl and getMcpTools Co-Authored-By: Claude Opus 4.5 * refactor: Use AI SDK built-in SSE transport for MCP Replace StreamableHTTPClientTransport with AI SDK's built-in transport config which provides a simpler API: transport: { type: "sse", url: "...", headers: { Authorization: "..." } } This should fix the "Streamable HTTP error" issue. Co-Authored-By: Claude Opus 4.5 * fix: Correct MCP endpoint URL to /mcp The MCP route is at app/mcp/route.ts, not app/api/mcp/route.ts. Co-Authored-By: Claude Opus 4.5 * fix: Use HTTP transport instead of SSE for MCP The mcp-handler server only supports HTTP transport, not SSE. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../__tests__/setupToolsForRequest.test.ts | 79 ++++++------------- lib/chat/setupToolsForRequest.ts | 23 ++---- lib/chat/validateChatRequest.ts | 5 ++ lib/mcp/__tests__/getMcpTools.test.ts | 63 +++++++++++++++ lib/mcp/getMcpTools.ts | 23 ++++++ lib/networking/__tests__/getBaseUrl.test.ts | 39 +++++++++ lib/networking/getBaseUrl.ts | 12 +++ 7 files changed, 171 insertions(+), 73 deletions(-) create mode 100644 lib/mcp/__tests__/getMcpTools.test.ts create mode 100644 lib/mcp/getMcpTools.ts create mode 100644 lib/networking/__tests__/getBaseUrl.test.ts create mode 100644 lib/networking/getBaseUrl.ts diff --git a/lib/chat/__tests__/setupToolsForRequest.test.ts b/lib/chat/__tests__/setupToolsForRequest.test.ts index d4107b30..c2d36bb8 100644 --- a/lib/chat/__tests__/setupToolsForRequest.test.ts +++ b/lib/chat/__tests__/setupToolsForRequest.test.ts @@ -2,28 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ChatRequestBody } from "../validateChatRequest"; // Mock external dependencies -vi.mock("@ai-sdk/mcp", () => ({ - experimental_createMCPClient: vi.fn(), -})); - -vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: vi.fn().mockImplementation(() => ({})), -})); - -vi.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({ - McpServer: vi.fn().mockImplementation(() => ({ - connect: vi.fn(), - })), -})); - -vi.mock("@modelcontextprotocol/sdk/inMemory.js", () => ({ - InMemoryTransport: { - createLinkedPair: vi.fn().mockReturnValue([{}, {}]), - }, -})); - -vi.mock("@/lib/mcp/tools", () => ({ - registerAllTools: vi.fn(), +vi.mock("@/lib/mcp/getMcpTools", () => ({ + getMcpTools: vi.fn(), })); vi.mock("@/lib/agents/googleSheetsAgent", () => ({ @@ -32,10 +12,10 @@ vi.mock("@/lib/agents/googleSheetsAgent", () => ({ // Import after mocks import { setupToolsForRequest } from "../setupToolsForRequest"; -import { experimental_createMCPClient } from "@ai-sdk/mcp"; +import { getMcpTools } from "@/lib/mcp/getMcpTools"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; -const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); +const mockGetMcpTools = vi.mocked(getMcpTools); const mockGetGoogleSheetsTools = vi.mocked(getGoogleSheetsTools); describe("setupToolsForRequest", () => { @@ -56,32 +36,32 @@ describe("setupToolsForRequest", () => { beforeEach(() => { vi.clearAllMocks(); - // Default mock for MCP client - mockCreateMCPClient.mockResolvedValue({ - tools: vi.fn().mockResolvedValue(mockMcpTools), - } as any); + // Default mock for MCP tools + mockGetMcpTools.mockResolvedValue(mockMcpTools); // Default mock for Google Sheets tools - returns login tool (not authenticated) mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsLoginTool); }); describe("MCP tools integration", () => { - it("creates MCP client with correct URL", async () => { + it("calls getMcpTools with authToken", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; await setupToolsForRequest(body); - expect(mockCreateMCPClient).toHaveBeenCalled(); + expect(mockGetMcpTools).toHaveBeenCalledWith("test-token-123"); }); it("fetches tools from MCP client", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -91,7 +71,7 @@ describe("setupToolsForRequest", () => { expect(result).toHaveProperty("tool2"); }); - it("passes accountId to MCP client via authenticated transport", async () => { + it("skips MCP tools when authToken is not provided", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -100,25 +80,7 @@ describe("setupToolsForRequest", () => { await setupToolsForRequest(body); - // Verify MCP client was created with a transport that includes auth info - expect(mockCreateMCPClient).toHaveBeenCalledWith( - expect.objectContaining({ - transport: expect.any(Object), - }), - ); - }); - - it("passes orgId to MCP client via authenticated transport", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: "org-456", - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - await setupToolsForRequest(body); - - // Verify MCP client was created - expect(mockCreateMCPClient).toHaveBeenCalled(); + expect(mockGetMcpTools).not.toHaveBeenCalled(); }); }); @@ -127,6 +89,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Create a spreadsheet" }], }; @@ -141,6 +104,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Create a spreadsheet" }], }; @@ -156,6 +120,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Create a spreadsheet" }], }; @@ -172,6 +137,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -185,11 +151,9 @@ describe("setupToolsForRequest", () => { }); it("Google Sheets tools take precedence over MCP tools with same name", async () => { - mockCreateMCPClient.mockResolvedValue({ - tools: vi.fn().mockResolvedValue({ - googlesheets_create: { description: "MCP version", parameters: {} }, - }), - } as any); + mockGetMcpTools.mockResolvedValue({ + googlesheets_create: { description: "MCP version", parameters: {} }, + }); mockGetGoogleSheetsTools.mockResolvedValue({ googlesheets_create: { description: "Composio version", parameters: {} }, @@ -198,6 +162,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -215,6 +180,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], excludeTools: ["tool1"], }; @@ -231,6 +197,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], excludeTools: ["tool1", "googlesheets_create"], }; @@ -247,6 +214,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -260,6 +228,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], excludeTools: [], }; diff --git a/lib/chat/setupToolsForRequest.ts b/lib/chat/setupToolsForRequest.ts index a5fac3d0..15746300 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -1,36 +1,23 @@ import { ToolSet } from "ai"; import { filterExcludedTools } from "./filterExcludedTools"; import { ChatRequestBody } from "./validateChatRequest"; -import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import { registerAllTools } from "@/lib/mcp/tools"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; +import { getMcpTools } from "@/lib/mcp/getMcpTools"; /** * Sets up and filters tools for a chat request. * Aggregates tools from: - * - MCP server (in-process via in-memory transport, no HTTP overhead) + * - MCP server (via HTTP transport to /api/mcp for proper auth) * - Google Sheets (via Composio integration) * * @param body - The chat request body * @returns Filtered tool set ready for use */ export async function setupToolsForRequest(body: ChatRequestBody): Promise { - const { excludeTools } = body; + const { excludeTools, authToken } = body; - // Create in-memory MCP server and client (no HTTP call needed) - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - - const server = new McpServer({ - name: "recoup-mcp", - version: "0.0.1", - }); - registerAllTools(server); - await server.connect(serverTransport); - - const mcpClient = await createMCPClient({ transport: clientTransport }); - const mcpClientTools = (await mcpClient.tools()) as ToolSet; + // Only fetch MCP tools if we have an auth token + const mcpClientTools = authToken ? await getMcpTools(authToken) : {}; // Fetch Google Sheets tools (authenticated tools or login tool) const googleSheetsTools = await getGoogleSheetsTools(body); diff --git a/lib/chat/validateChatRequest.ts b/lib/chat/validateChatRequest.ts index c1a7ec6b..114656c3 100644 --- a/lib/chat/validateChatRequest.ts +++ b/lib/chat/validateChatRequest.ts @@ -48,6 +48,7 @@ type BaseChatRequestBody = z.infer; export type ChatRequestBody = BaseChatRequestBody & { accountId: string; orgId: string | null; + authToken?: string; }; /** @@ -192,10 +193,14 @@ export async function validateChatRequest( memoryId: lastMessage.id, }); + // Extract the auth token to forward to MCP server + const authToken = hasApiKey ? apiKey! : authHeader!.replace(/^Bearer\s+/i, ""); + return { ...validatedBody, accountId, orgId, roomId: finalRoomId, + authToken, } as ChatRequestBody; } diff --git a/lib/mcp/__tests__/getMcpTools.test.ts b/lib/mcp/__tests__/getMcpTools.test.ts new file mode 100644 index 00000000..0422fbc0 --- /dev/null +++ b/lib/mcp/__tests__/getMcpTools.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@ai-sdk/mcp", () => ({ + experimental_createMCPClient: vi.fn(), +})); + +vi.mock("@/lib/networking/getBaseUrl", () => ({ + getBaseUrl: vi.fn().mockReturnValue("https://test.vercel.app"), +})); + +import { getMcpTools } from "../getMcpTools"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; + +const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); + +describe("getMcpTools", () => { + const mockTools = { + tool1: { description: "Tool 1", parameters: {} }, + tool2: { description: "Tool 2", parameters: {} }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + + mockCreateMCPClient.mockResolvedValue({ + tools: vi.fn().mockResolvedValue(mockTools), + } as any); + }); + + it("creates MCP client with HTTP transport config", async () => { + await getMcpTools("test-token"); + + expect(mockCreateMCPClient).toHaveBeenCalledWith({ + transport: { + type: "http", + url: "https://test.vercel.app/mcp", + headers: { + Authorization: "Bearer test-token", + }, + }, + }); + }); + + it("returns tools from MCP client", async () => { + const result = await getMcpTools("test-token"); + + expect(result).toEqual(mockTools); + }); + + it("passes different auth tokens correctly", async () => { + await getMcpTools("different-token"); + + expect(mockCreateMCPClient).toHaveBeenCalledWith({ + transport: { + type: "http", + url: "https://test.vercel.app/mcp", + headers: { + Authorization: "Bearer different-token", + }, + }, + }); + }); +}); diff --git a/lib/mcp/getMcpTools.ts b/lib/mcp/getMcpTools.ts new file mode 100644 index 00000000..2e1fae6a --- /dev/null +++ b/lib/mcp/getMcpTools.ts @@ -0,0 +1,23 @@ +import { ToolSet } from "ai"; +import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; +import { getBaseUrl } from "@/lib/networking/getBaseUrl"; + +/** + * Fetches MCP tools via HTTP transport with authentication. + * + * @param authToken - The auth token to use for MCP endpoint authentication + * @returns The MCP tools as a ToolSet + */ +export async function getMcpTools(authToken: string): Promise { + const mcpClient = await createMCPClient({ + transport: { + type: "http", + url: `${getBaseUrl()}/mcp`, + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + }); + + return (await mcpClient.tools()) as ToolSet; +} diff --git a/lib/networking/__tests__/getBaseUrl.test.ts b/lib/networking/__tests__/getBaseUrl.test.ts new file mode 100644 index 00000000..fa678ea1 --- /dev/null +++ b/lib/networking/__tests__/getBaseUrl.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { getBaseUrl } from "../getBaseUrl"; + +describe("getBaseUrl", () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("returns HTTPS URL when VERCEL_URL is set", () => { + process.env.VERCEL_URL = "my-app.vercel.app"; + + const result = getBaseUrl(); + + expect(result).toBe("https://my-app.vercel.app"); + }); + + it("returns localhost when VERCEL_URL is not set", () => { + delete process.env.VERCEL_URL; + + const result = getBaseUrl(); + + expect(result).toBe("http://localhost:3000"); + }); + + it("returns localhost when VERCEL_URL is empty string", () => { + process.env.VERCEL_URL = ""; + + const result = getBaseUrl(); + + expect(result).toBe("http://localhost:3000"); + }); +}); diff --git a/lib/networking/getBaseUrl.ts b/lib/networking/getBaseUrl.ts new file mode 100644 index 00000000..112d94e5 --- /dev/null +++ b/lib/networking/getBaseUrl.ts @@ -0,0 +1,12 @@ +/** + * Gets the base URL for the current API server. + * Uses VERCEL_URL in Vercel deployments, falls back to localhost. + * + * @returns The base URL string + */ +export function getBaseUrl(): string { + if (process.env.VERCEL_URL) { + return `https://${process.env.VERCEL_URL}`; + } + return "http://localhost:3000"; +}