From 7b4247cb9c9ada91581703a85dc2203e9057399d Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 12:58:51 -0500 Subject: [PATCH 1/8] 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 --- .../__tests__/setupToolsForRequest.test.ts | 59 ++++++++----------- lib/chat/setupToolsForRequest.ts | 39 +++++++----- lib/chat/validateChatRequest.ts | 5 ++ 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/lib/chat/__tests__/setupToolsForRequest.test.ts b/lib/chat/__tests__/setupToolsForRequest.test.ts index d4107b30..80439e31 100644 --- a/lib/chat/__tests__/setupToolsForRequest.test.ts +++ b/lib/chat/__tests__/setupToolsForRequest.test.ts @@ -10,22 +10,6 @@ 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/agents/googleSheetsAgent", () => ({ getGoogleSheetsTools: vi.fn(), })); @@ -34,9 +18,11 @@ vi.mock("@/lib/agents/googleSheetsAgent", () => ({ import { setupToolsForRequest } from "../setupToolsForRequest"; import { experimental_createMCPClient } from "@ai-sdk/mcp"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); const mockGetGoogleSheetsTools = vi.mocked(getGoogleSheetsTools); +const mockStreamableHTTPClientTransport = vi.mocked(StreamableHTTPClientTransport); describe("setupToolsForRequest", () => { const mockMcpTools = { @@ -66,15 +52,17 @@ describe("setupToolsForRequest", () => { }); describe("MCP tools integration", () => { - it("creates MCP client with correct URL", async () => { + it("creates MCP client with HTTP transport", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; await setupToolsForRequest(body); + expect(mockStreamableHTTPClientTransport).toHaveBeenCalled(); expect(mockCreateMCPClient).toHaveBeenCalled(); }); @@ -82,6 +70,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -91,35 +80,28 @@ describe("setupToolsForRequest", () => { expect(result).toHaveProperty("tool2"); }); - it("passes accountId to MCP client via authenticated transport", async () => { + it("passes authToken to MCP client via HTTP transport", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; await setupToolsForRequest(body); - // Verify MCP client was created with a transport that includes auth info - expect(mockCreateMCPClient).toHaveBeenCalledWith( + // Verify HTTP transport was created with auth header + expect(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( + expect.any(URL), expect.objectContaining({ - transport: expect.any(Object), + requestInit: { + headers: { + Authorization: "Bearer test-token-123", + }, + }, }), ); }); - - 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(); - }); }); describe("Google Sheets tools integration", () => { @@ -127,6 +109,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 +124,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 +140,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 +157,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -198,6 +184,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -215,6 +202,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 +219,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 +236,7 @@ describe("setupToolsForRequest", () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, + authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; @@ -260,6 +250,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..40a0431b 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -2,34 +2,45 @@ 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 { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; +/** + * Gets the base URL for the current environment. + * Uses VERCEL_URL in Vercel deployments, falls back to localhost. + * + * @returns The base URL string + */ +function getBaseUrl(): string { + if (process.env.VERCEL_URL) { + return `https://${process.env.VERCEL_URL}`; + } + return "http://localhost:3000"; +} + /** * 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; - - // Create in-memory MCP server and client (no HTTP call needed) - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const { excludeTools, authToken } = body; - const server = new McpServer({ - name: "recoup-mcp", - version: "0.0.1", + // Create HTTP transport to MCP endpoint with forwarded auth token + const mcpUrl = new URL("/api/mcp", getBaseUrl()); + const transport = new StreamableHTTPClientTransport(mcpUrl, { + requestInit: { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, }); - registerAllTools(server); - await server.connect(serverTransport); - const mcpClient = await createMCPClient({ transport: clientTransport }); + const mcpClient = await createMCPClient({ transport }); const mcpClientTools = (await mcpClient.tools()) as ToolSet; // Fetch Google Sheets tools (authenticated tools or login tool) diff --git a/lib/chat/validateChatRequest.ts b/lib/chat/validateChatRequest.ts index c1a7ec6b..ad4c9318 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; } From 297aaca24ea61b754411b04be114ec0d4e0c6303 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:02:21 -0500 Subject: [PATCH 2/8] 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 --- lib/chat/setupToolsForRequest.ts | 24 ++++++++++++++---------- lib/chat/validateChatRequest.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/chat/setupToolsForRequest.ts b/lib/chat/setupToolsForRequest.ts index 40a0431b..f57a8a66 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -30,18 +30,22 @@ function getBaseUrl(): string { export async function setupToolsForRequest(body: ChatRequestBody): Promise { const { excludeTools, authToken } = body; - // Create HTTP transport to MCP endpoint with forwarded auth token - const mcpUrl = new URL("/api/mcp", getBaseUrl()); - const transport = new StreamableHTTPClientTransport(mcpUrl, { - requestInit: { - headers: { - Authorization: `Bearer ${authToken}`, + let mcpClientTools: ToolSet = {}; + + // Only fetch MCP tools if we have an auth token + if (authToken) { + const mcpUrl = new URL("/api/mcp", getBaseUrl()); + const transport = new StreamableHTTPClientTransport(mcpUrl, { + requestInit: { + headers: { + Authorization: `Bearer ${authToken}`, + }, }, - }, - }); + }); - const mcpClient = await createMCPClient({ transport }); - const mcpClientTools = (await mcpClient.tools()) as ToolSet; + const mcpClient = await createMCPClient({ transport }); + mcpClientTools = (await mcpClient.tools()) as ToolSet; + } // 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 ad4c9318..114656c3 100644 --- a/lib/chat/validateChatRequest.ts +++ b/lib/chat/validateChatRequest.ts @@ -48,7 +48,7 @@ type BaseChatRequestBody = z.infer; export type ChatRequestBody = BaseChatRequestBody & { accountId: string; orgId: string | null; - authToken: string; + authToken?: string; }; /** From e79399380d12734c6da04b3366db757004be5f45 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:03:41 -0500 Subject: [PATCH 3/8] refactor: Extract getBaseUrl to lib/networking Move getBaseUrl utility to its own file following SRP. Co-Authored-By: Claude Opus 4.5 --- lib/chat/setupToolsForRequest.ts | 14 +------------- lib/networking/getBaseUrl.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 lib/networking/getBaseUrl.ts diff --git a/lib/chat/setupToolsForRequest.ts b/lib/chat/setupToolsForRequest.ts index f57a8a66..4d433d7b 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -4,19 +4,7 @@ import { ChatRequestBody } from "./validateChatRequest"; import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; - -/** - * Gets the base URL for the current environment. - * Uses VERCEL_URL in Vercel deployments, falls back to localhost. - * - * @returns The base URL string - */ -function getBaseUrl(): string { - if (process.env.VERCEL_URL) { - return `https://${process.env.VERCEL_URL}`; - } - return "http://localhost:3000"; -} +import { getBaseUrl } from "@/lib/networking/getBaseUrl"; /** * Sets up and filters tools for a chat request. 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"; +} From c94fc0411aea38d1bab4066b2c6ed7c90c1ce074 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:05:44 -0500 Subject: [PATCH 4/8] refactor: Extract getMcpTools to lib/mcp Move MCP tools fetching logic to its own file following SRP. Co-Authored-By: Claude Opus 4.5 --- lib/chat/setupToolsForRequest.ts | 20 ++------------------ lib/mcp/getMcpTools.ts | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 lib/mcp/getMcpTools.ts diff --git a/lib/chat/setupToolsForRequest.ts b/lib/chat/setupToolsForRequest.ts index 4d433d7b..15746300 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -1,10 +1,8 @@ import { ToolSet } from "ai"; import { filterExcludedTools } from "./filterExcludedTools"; import { ChatRequestBody } from "./validateChatRequest"; -import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; -import { getBaseUrl } from "@/lib/networking/getBaseUrl"; +import { getMcpTools } from "@/lib/mcp/getMcpTools"; /** * Sets up and filters tools for a chat request. @@ -18,22 +16,8 @@ import { getBaseUrl } from "@/lib/networking/getBaseUrl"; export async function setupToolsForRequest(body: ChatRequestBody): Promise { const { excludeTools, authToken } = body; - let mcpClientTools: ToolSet = {}; - // Only fetch MCP tools if we have an auth token - if (authToken) { - const mcpUrl = new URL("/api/mcp", getBaseUrl()); - const transport = new StreamableHTTPClientTransport(mcpUrl, { - requestInit: { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }, - }); - - const mcpClient = await createMCPClient({ transport }); - mcpClientTools = (await mcpClient.tools()) as ToolSet; - } + const mcpClientTools = authToken ? await getMcpTools(authToken) : {}; // Fetch Google Sheets tools (authenticated tools or login tool) const googleSheetsTools = await getGoogleSheetsTools(body); diff --git a/lib/mcp/getMcpTools.ts b/lib/mcp/getMcpTools.ts new file mode 100644 index 00000000..3fdb27f1 --- /dev/null +++ b/lib/mcp/getMcpTools.ts @@ -0,0 +1,24 @@ +import { ToolSet } from "ai"; +import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +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 mcpUrl = new URL("/api/mcp", getBaseUrl()); + const transport = new StreamableHTTPClientTransport(mcpUrl, { + requestInit: { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + }); + + const mcpClient = await createMCPClient({ transport }); + return (await mcpClient.tools()) as ToolSet; +} From 6ce9df71c522aedc90beaac4d543b694d25caaef Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:06:42 -0500 Subject: [PATCH 5/8] test: Add unit tests for getBaseUrl and getMcpTools Co-Authored-By: Claude Opus 4.5 --- lib/mcp/__tests__/getMcpTools.test.ts | 83 +++++++++++++++++++++ lib/networking/__tests__/getBaseUrl.test.ts | 39 ++++++++++ 2 files changed, 122 insertions(+) create mode 100644 lib/mcp/__tests__/getMcpTools.test.ts create mode 100644 lib/networking/__tests__/getBaseUrl.test.ts diff --git a/lib/mcp/__tests__/getMcpTools.test.ts b/lib/mcp/__tests__/getMcpTools.test.ts new file mode 100644 index 00000000..f8fd16b2 --- /dev/null +++ b/lib/mcp/__tests__/getMcpTools.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@ai-sdk/mcp", () => ({ + experimental_createMCPClient: vi.fn(), +})); + +vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: vi.fn().mockImplementation(() => ({})), +})); + +vi.mock("@/lib/networking/getBaseUrl", () => ({ + getBaseUrl: vi.fn().mockReturnValue("https://test.vercel.app"), +})); + +import { getMcpTools } from "../getMcpTools"; +import { experimental_createMCPClient } from "@ai-sdk/mcp"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); +const mockStreamableHTTPClientTransport = vi.mocked(StreamableHTTPClientTransport); + +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 HTTP transport with correct URL", async () => { + await getMcpTools("test-token"); + + expect(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + requestInit: { + headers: { + Authorization: "Bearer test-token", + }, + }, + }), + ); + + const urlArg = mockStreamableHTTPClientTransport.mock.calls[0][0] as URL; + expect(urlArg.pathname).toBe("/api/mcp"); + expect(urlArg.origin).toBe("https://test.vercel.app"); + }); + + it("creates MCP client with transport", async () => { + await getMcpTools("test-token"); + + expect(mockCreateMCPClient).toHaveBeenCalledWith({ + transport: expect.any(Object), + }); + }); + + 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(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + requestInit: { + headers: { + Authorization: "Bearer different-token", + }, + }, + }), + ); + }); +}); 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"); + }); +}); From 5ea3ff4dab8ab7b104b031881a141661addb483c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:13:56 -0500 Subject: [PATCH 6/8] 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 --- .../__tests__/setupToolsForRequest.test.ts | 48 +++++------------ lib/mcp/__tests__/getMcpTools.test.ts | 52 ++++++------------- lib/mcp/getMcpTools.ts | 9 ++-- 3 files changed, 33 insertions(+), 76 deletions(-) diff --git a/lib/chat/__tests__/setupToolsForRequest.test.ts b/lib/chat/__tests__/setupToolsForRequest.test.ts index 80439e31..c2d36bb8 100644 --- a/lib/chat/__tests__/setupToolsForRequest.test.ts +++ b/lib/chat/__tests__/setupToolsForRequest.test.ts @@ -2,12 +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("@/lib/mcp/getMcpTools", () => ({ + getMcpTools: vi.fn(), })); vi.mock("@/lib/agents/googleSheetsAgent", () => ({ @@ -16,13 +12,11 @@ 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"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); +const mockGetMcpTools = vi.mocked(getMcpTools); const mockGetGoogleSheetsTools = vi.mocked(getGoogleSheetsTools); -const mockStreamableHTTPClientTransport = vi.mocked(StreamableHTTPClientTransport); describe("setupToolsForRequest", () => { const mockMcpTools = { @@ -42,17 +36,15 @@ 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 HTTP transport", async () => { + it("calls getMcpTools with authToken", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -62,8 +54,7 @@ describe("setupToolsForRequest", () => { await setupToolsForRequest(body); - expect(mockStreamableHTTPClientTransport).toHaveBeenCalled(); - expect(mockCreateMCPClient).toHaveBeenCalled(); + expect(mockGetMcpTools).toHaveBeenCalledWith("test-token-123"); }); it("fetches tools from MCP client", async () => { @@ -80,27 +71,16 @@ describe("setupToolsForRequest", () => { expect(result).toHaveProperty("tool2"); }); - it("passes authToken to MCP client via HTTP transport", async () => { + it("skips MCP tools when authToken is not provided", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, - authToken: "test-token-123", messages: [{ id: "1", role: "user", content: "Hello" }], }; await setupToolsForRequest(body); - // Verify HTTP transport was created with auth header - expect(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( - expect.any(URL), - expect.objectContaining({ - requestInit: { - headers: { - Authorization: "Bearer test-token-123", - }, - }, - }), - ); + expect(mockGetMcpTools).not.toHaveBeenCalled(); }); }); @@ -171,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: {} }, diff --git a/lib/mcp/__tests__/getMcpTools.test.ts b/lib/mcp/__tests__/getMcpTools.test.ts index f8fd16b2..b379224d 100644 --- a/lib/mcp/__tests__/getMcpTools.test.ts +++ b/lib/mcp/__tests__/getMcpTools.test.ts @@ -4,20 +4,14 @@ vi.mock("@ai-sdk/mcp", () => ({ experimental_createMCPClient: vi.fn(), })); -vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: vi.fn().mockImplementation(() => ({})), -})); - vi.mock("@/lib/networking/getBaseUrl", () => ({ getBaseUrl: vi.fn().mockReturnValue("https://test.vercel.app"), })); import { getMcpTools } from "../getMcpTools"; import { experimental_createMCPClient } from "@ai-sdk/mcp"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const mockCreateMCPClient = vi.mocked(experimental_createMCPClient); -const mockStreamableHTTPClientTransport = vi.mocked(StreamableHTTPClientTransport); describe("getMcpTools", () => { const mockTools = { @@ -33,30 +27,17 @@ describe("getMcpTools", () => { } as any); }); - it("creates HTTP transport with correct URL", async () => { - await getMcpTools("test-token"); - - expect(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( - expect.any(URL), - expect.objectContaining({ - requestInit: { - headers: { - Authorization: "Bearer test-token", - }, - }, - }), - ); - - const urlArg = mockStreamableHTTPClientTransport.mock.calls[0][0] as URL; - expect(urlArg.pathname).toBe("/api/mcp"); - expect(urlArg.origin).toBe("https://test.vercel.app"); - }); - - it("creates MCP client with transport", async () => { + it("creates MCP client with SSE transport config", async () => { await getMcpTools("test-token"); expect(mockCreateMCPClient).toHaveBeenCalledWith({ - transport: expect.any(Object), + transport: { + type: "sse", + url: "https://test.vercel.app/api/mcp", + headers: { + Authorization: "Bearer test-token", + }, + }, }); }); @@ -69,15 +50,14 @@ describe("getMcpTools", () => { it("passes different auth tokens correctly", async () => { await getMcpTools("different-token"); - expect(mockStreamableHTTPClientTransport).toHaveBeenCalledWith( - expect.any(URL), - expect.objectContaining({ - requestInit: { - headers: { - Authorization: "Bearer different-token", - }, + expect(mockCreateMCPClient).toHaveBeenCalledWith({ + transport: { + type: "sse", + url: "https://test.vercel.app/api/mcp", + headers: { + Authorization: "Bearer different-token", }, - }), - ); + }, + }); }); }); diff --git a/lib/mcp/getMcpTools.ts b/lib/mcp/getMcpTools.ts index 3fdb27f1..3de98329 100644 --- a/lib/mcp/getMcpTools.ts +++ b/lib/mcp/getMcpTools.ts @@ -1,6 +1,5 @@ import { ToolSet } from "ai"; import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { getBaseUrl } from "@/lib/networking/getBaseUrl"; /** @@ -10,15 +9,15 @@ import { getBaseUrl } from "@/lib/networking/getBaseUrl"; * @returns The MCP tools as a ToolSet */ export async function getMcpTools(authToken: string): Promise { - const mcpUrl = new URL("/api/mcp", getBaseUrl()); - const transport = new StreamableHTTPClientTransport(mcpUrl, { - requestInit: { + const mcpClient = await createMCPClient({ + transport: { + type: "sse", + url: `${getBaseUrl()}/api/mcp`, headers: { Authorization: `Bearer ${authToken}`, }, }, }); - const mcpClient = await createMCPClient({ transport }); return (await mcpClient.tools()) as ToolSet; } From e6663c16d9f031acc2f191a7b6ea62cd3a02d6c8 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:16:19 -0500 Subject: [PATCH 7/8] 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 --- lib/mcp/__tests__/getMcpTools.test.ts | 4 ++-- lib/mcp/getMcpTools.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mcp/__tests__/getMcpTools.test.ts b/lib/mcp/__tests__/getMcpTools.test.ts index b379224d..fa5199a4 100644 --- a/lib/mcp/__tests__/getMcpTools.test.ts +++ b/lib/mcp/__tests__/getMcpTools.test.ts @@ -33,7 +33,7 @@ describe("getMcpTools", () => { expect(mockCreateMCPClient).toHaveBeenCalledWith({ transport: { type: "sse", - url: "https://test.vercel.app/api/mcp", + url: "https://test.vercel.app/mcp", headers: { Authorization: "Bearer test-token", }, @@ -53,7 +53,7 @@ describe("getMcpTools", () => { expect(mockCreateMCPClient).toHaveBeenCalledWith({ transport: { type: "sse", - url: "https://test.vercel.app/api/mcp", + url: "https://test.vercel.app/mcp", headers: { Authorization: "Bearer different-token", }, diff --git a/lib/mcp/getMcpTools.ts b/lib/mcp/getMcpTools.ts index 3de98329..270087bf 100644 --- a/lib/mcp/getMcpTools.ts +++ b/lib/mcp/getMcpTools.ts @@ -12,7 +12,7 @@ export async function getMcpTools(authToken: string): Promise { const mcpClient = await createMCPClient({ transport: { type: "sse", - url: `${getBaseUrl()}/api/mcp`, + url: `${getBaseUrl()}/mcp`, headers: { Authorization: `Bearer ${authToken}`, }, From 076e86cbaabba8fa3c2ff85432ac96cb7f30fcfe Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 20 Jan 2026 13:20:05 -0500 Subject: [PATCH 8/8] 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 --- lib/mcp/__tests__/getMcpTools.test.ts | 6 +++--- lib/mcp/getMcpTools.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mcp/__tests__/getMcpTools.test.ts b/lib/mcp/__tests__/getMcpTools.test.ts index fa5199a4..0422fbc0 100644 --- a/lib/mcp/__tests__/getMcpTools.test.ts +++ b/lib/mcp/__tests__/getMcpTools.test.ts @@ -27,12 +27,12 @@ describe("getMcpTools", () => { } as any); }); - it("creates MCP client with SSE transport config", async () => { + it("creates MCP client with HTTP transport config", async () => { await getMcpTools("test-token"); expect(mockCreateMCPClient).toHaveBeenCalledWith({ transport: { - type: "sse", + type: "http", url: "https://test.vercel.app/mcp", headers: { Authorization: "Bearer test-token", @@ -52,7 +52,7 @@ describe("getMcpTools", () => { expect(mockCreateMCPClient).toHaveBeenCalledWith({ transport: { - type: "sse", + 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 index 270087bf..2e1fae6a 100644 --- a/lib/mcp/getMcpTools.ts +++ b/lib/mcp/getMcpTools.ts @@ -11,7 +11,7 @@ import { getBaseUrl } from "@/lib/networking/getBaseUrl"; export async function getMcpTools(authToken: string): Promise { const mcpClient = await createMCPClient({ transport: { - type: "sse", + type: "http", url: `${getBaseUrl()}/mcp`, headers: { Authorization: `Bearer ${authToken}`,