diff --git a/app/api/connectedAccounts/refresh/route.ts b/app/api/connectedAccounts/refresh/route.ts deleted file mode 100644 index e2e70820..00000000 --- a/app/api/connectedAccounts/refresh/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import refreshConnectedAccount from "@/lib/composio/googleSheets/refreshConnectedAccount"; -import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; - -/** - * OPTIONS handler for CORS preflight requests. - * - * @returns A NextResponse with CORS headers. - */ -export async function OPTIONS() { - return new NextResponse(null, { - status: 200, - headers: getCorsHeaders(), - }); -} - -/** - * POST handler for refreshing a connected account. - * - * @param request - The request object. - * @returns A NextResponse with the refreshed connected account. - */ -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { accountId, redirectUrl } = body; - - if (!accountId) { - return NextResponse.json( - { error: "accountId is required" }, - { - status: 400, - headers: getCorsHeaders(), - }, - ); - } - - const response = await refreshConnectedAccount(accountId, redirectUrl); - - return NextResponse.json( - { message: "Connected account refreshed successfully", ...response }, - { - status: 200, - headers: getCorsHeaders(), - }, - ); - } catch (error) { - console.error("Error refreshing connected account:", error); - - const errorMessage = error instanceof Error ? error.message : "Internal server error"; - const statusCode = errorMessage.includes("not found") ? 404 : 500; - - return NextResponse.json( - { error: errorMessage }, - { - status: statusCode, - headers: getCorsHeaders(), - }, - ); - } -} diff --git a/app/api/connectors/authorize/route.ts b/app/api/connectors/authorize/route.ts index 5bf54d6b..1d539a12 100644 --- a/app/api/connectors/authorize/route.ts +++ b/app/api/connectors/authorize/route.ts @@ -2,7 +2,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { authorizeConnector } from "@/lib/composio/connectors"; -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateAccountIdHeaders } from "@/lib/accounts/validateAccountIdHeaders"; import { validateAuthorizeConnectorBody } from "@/lib/composio/connectors/validateAuthorizeConnectorBody"; /** @@ -20,8 +20,8 @@ export async function OPTIONS() { * * Generate an OAuth authorization URL for a specific connector. * - * Authentication: x-api-key header required. - * The account ID is inferred from the API key. + * Authentication: x-api-key OR Authorization Bearer token required. + * The account ID is inferred from the auth header. * * Request body: * - connector: The connector slug, e.g., "googlesheets" (required) @@ -33,12 +33,12 @@ export async function POST(request: NextRequest): Promise { const headers = getCorsHeaders(); try { - const accountIdOrError = await getApiKeyAccountId(request); - if (accountIdOrError instanceof NextResponse) { - return accountIdOrError; + const authResult = await validateAccountIdHeaders(request); + if (authResult instanceof NextResponse) { + return authResult; } - const accountId = accountIdOrError; + const { accountId } = authResult; const body = await request.json(); const validated = validateAuthorizeConnectorBody(body); diff --git a/app/api/connectors/route.ts b/app/api/connectors/route.ts index 8dfdab95..bdafa4e0 100644 --- a/app/api/connectors/route.ts +++ b/app/api/connectors/route.ts @@ -5,7 +5,7 @@ import { getConnectors } from "@/lib/composio/connectors"; import { disconnectConnector } from "@/lib/composio/connectors/disconnectConnector"; import { validateDisconnectConnectorBody } from "@/lib/composio/connectors/validateDisconnectConnectorBody"; import { verifyConnectorOwnership } from "@/lib/composio/connectors/verifyConnectorOwnership"; -import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { validateAccountIdHeaders } from "@/lib/accounts/validateAccountIdHeaders"; /** * OPTIONS handler for CORS preflight requests. @@ -22,7 +22,7 @@ export async function OPTIONS() { * * List all available connectors and their connection status for a user. * - * Authentication: x-api-key header required. + * Authentication: x-api-key OR Authorization Bearer token required. * * @returns List of connectors with connection status */ @@ -30,12 +30,12 @@ export async function GET(request: NextRequest): Promise { const headers = getCorsHeaders(); try { - const accountIdOrError = await getApiKeyAccountId(request); - if (accountIdOrError instanceof NextResponse) { - return accountIdOrError; + const authResult = await validateAccountIdHeaders(request); + if (authResult instanceof NextResponse) { + return authResult; } - const accountId = accountIdOrError; + const { accountId } = authResult; const connectors = await getConnectors(accountId); @@ -60,7 +60,7 @@ export async function GET(request: NextRequest): Promise { * * Disconnect a connected account from Composio. * - * Authentication: x-api-key header required. + * Authentication: x-api-key OR Authorization Bearer token required. * * Body: { connected_account_id: string } */ @@ -68,12 +68,12 @@ export async function DELETE(request: NextRequest): Promise { const headers = getCorsHeaders(); try { - const accountIdOrError = await getApiKeyAccountId(request); - if (accountIdOrError instanceof NextResponse) { - return accountIdOrError; + const authResult = await validateAccountIdHeaders(request); + if (authResult instanceof NextResponse) { + return authResult; } - const accountId = accountIdOrError; + const { accountId } = authResult; const body = await request.json(); const validated = validateDisconnectConnectorBody(body); diff --git a/lib/agents/googleSheetsAgent/__tests__/getGoogleSheetsTools.test.ts b/lib/agents/googleSheetsAgent/__tests__/getGoogleSheetsTools.test.ts deleted file mode 100644 index 35364c8f..00000000 --- a/lib/agents/googleSheetsAgent/__tests__/getGoogleSheetsTools.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; - -// Mock external dependencies -vi.mock("@/lib/composio/client", () => ({ - getComposioClient: vi.fn(), -})); - -vi.mock("@/lib/composio/googleSheets/getConnectedAccount", () => ({ - default: vi.fn(), - GOOGLE_SHEETS_TOOLKIT_SLUG: "GOOGLESHEETS", -})); - -vi.mock("@/lib/messages/getLatestUserMessageText", () => ({ - default: vi.fn(), -})); - -vi.mock("@/lib/composio/tools/googleSheetsLoginTool", () => ({ - default: { description: "Login to Google Sheets", parameters: {} }, -})); - -// Import after mocks -import getGoogleSheetsTools from "../getGoogleSheetsTools"; -import { getComposioClient } from "@/lib/composio/client"; -import getConnectedAccount from "@/lib/composio/googleSheets/getConnectedAccount"; -import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText"; - -const mockGetComposioClient = vi.mocked(getComposioClient); -const mockGetConnectedAccount = vi.mocked(getConnectedAccount); -const mockGetLatestUserMessageText = vi.mocked(getLatestUserMessageText); - -describe("getGoogleSheetsTools", () => { - const mockGoogleSheetsTools = { - googlesheets_create: { description: "Create sheet" }, - googlesheets_read: { description: "Read sheet" }, - }; - - beforeEach(() => { - vi.clearAllMocks(); - - // Default: not authenticated - mockGetConnectedAccount.mockResolvedValue({ - items: [], - } as any); - - mockGetLatestUserMessageText.mockReturnValue("Test message"); - - mockGetComposioClient.mockReturnValue({ - tools: { - get: vi.fn().mockResolvedValue(mockGoogleSheetsTools), - }, - } as any); - }); - - describe("authentication check", () => { - it("calls getConnectedAccount with accountId", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - await getGoogleSheetsTools(body); - - expect(mockGetConnectedAccount).toHaveBeenCalledWith( - "account-123", - expect.any(Object), - ); - }); - - it("passes callback URL with encoded latest user message", async () => { - mockGetLatestUserMessageText.mockReturnValue("Create a spreadsheet for me"); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Create a spreadsheet for me" }], - }; - - await getGoogleSheetsTools(body); - - expect(mockGetConnectedAccount).toHaveBeenCalledWith( - "account-123", - expect.objectContaining({ - callbackUrl: expect.stringContaining("Create%20a%20spreadsheet%20for%20me"), - }), - ); - }); - - it("callback URL uses chat.recoupable.com as base", async () => { - mockGetLatestUserMessageText.mockReturnValue("Test"); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Test" }], - }; - - await getGoogleSheetsTools(body); - - expect(mockGetConnectedAccount).toHaveBeenCalledWith( - "account-123", - expect.objectContaining({ - callbackUrl: expect.stringMatching(/^https:\/\/chat\.recoupable\.com/), - }), - ); - }); - }); - - describe("when user is authenticated", () => { - beforeEach(() => { - mockGetConnectedAccount.mockResolvedValue({ - items: [{ data: { status: "ACTIVE" } }], - } as any); - }); - - it("returns Google Sheets tools from Composio", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await getGoogleSheetsTools(body); - - expect(result).toHaveProperty("googlesheets_create"); - expect(result).toHaveProperty("googlesheets_read"); - }); - - it("calls composio.tools.get with correct parameters", async () => { - const mockToolsGet = vi.fn().mockResolvedValue(mockGoogleSheetsTools); - mockGetComposioClient.mockReturnValue({ - tools: { get: mockToolsGet }, - } as any); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - await getGoogleSheetsTools(body); - - expect(mockToolsGet).toHaveBeenCalledWith("account-123", { - toolkits: ["GOOGLESHEETS"], - }); - }); - }); - - describe("when user is not authenticated", () => { - beforeEach(() => { - mockGetConnectedAccount.mockResolvedValue({ - items: [], - } as any); - }); - - it("returns google_sheets_login tool", async () => { - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await getGoogleSheetsTools(body); - - expect(result).toHaveProperty("google_sheets_login"); - }); - - it("does not call composio.tools.get", async () => { - const mockToolsGet = vi.fn().mockResolvedValue(mockGoogleSheetsTools); - mockGetComposioClient.mockReturnValue({ - tools: { get: mockToolsGet }, - } as any); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - await getGoogleSheetsTools(body); - - expect(mockToolsGet).not.toHaveBeenCalled(); - }); - }); - - describe("when connection status is not ACTIVE", () => { - it("returns login tool when status is PENDING", async () => { - mockGetConnectedAccount.mockResolvedValue({ - items: [{ data: { status: "PENDING" } }], - } as any); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await getGoogleSheetsTools(body); - - expect(result).toHaveProperty("google_sheets_login"); - }); - - it("returns login tool when status is EXPIRED", async () => { - mockGetConnectedAccount.mockResolvedValue({ - items: [{ data: { status: "EXPIRED" } }], - } as any); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - messages: [{ id: "1", role: "user", content: "Hello" }], - }; - - const result = await getGoogleSheetsTools(body); - - expect(result).toHaveProperty("google_sheets_login"); - }); - }); - - describe("message text extraction", () => { - it("extracts text from messages using getLatestUserMessageText", async () => { - const messages = [ - { id: "1", role: "user", content: "First message" }, - { id: "2", role: "assistant", content: "Response" }, - { id: "3", role: "user", content: "Second message" }, - ]; - const body: ChatRequestBody = { - accountId: "account-123", - messages, - }; - - await getGoogleSheetsTools(body); - - expect(mockGetLatestUserMessageText).toHaveBeenCalledWith(messages); - }); - }); -}); diff --git a/lib/agents/googleSheetsAgent/getGoogleSheetsTools.ts b/lib/agents/googleSheetsAgent/getGoogleSheetsTools.ts deleted file mode 100644 index 1fb12012..00000000 --- a/lib/agents/googleSheetsAgent/getGoogleSheetsTools.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ToolSet } from "ai"; -import { CreateConnectedAccountOptions } from "@composio/core"; -import { getComposioClient } from "@/lib/composio/client"; -import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; -import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText"; -import getConnectedAccount, { - GOOGLE_SHEETS_TOOLKIT_SLUG, -} from "@/lib/composio/googleSheets/getConnectedAccount"; -import googleSheetsLoginTool from "@/lib/composio/tools/googleSheetsLoginTool"; - -/** Frontend callback URL for Google Sheets authentication */ -const CHAT_CALLBACK_URL = process.env.CHAT_CALLBACK_URL || "https://chat.recoupable.com"; - -/** - * Gets Google Sheets tools for a chat request. - * If the user is authenticated with Google Sheets, returns the full toolkit. - * Otherwise, returns a login tool to initiate authentication. - * - * @param body - The chat request body - * @returns ToolSet containing Google Sheets tools - */ -export default async function getGoogleSheetsTools(body: ChatRequestBody): Promise { - const { accountId, messages } = body; - - const latestUserMessageText = getLatestUserMessageText(messages); - - const options: CreateConnectedAccountOptions = { - callbackUrl: `${CHAT_CALLBACK_URL}?q=${encodeURIComponent(latestUserMessageText)}`, - }; - - const composio = getComposioClient(); - const userAccounts = await getConnectedAccount(accountId, options); - const isAuthenticated = userAccounts.items[0]?.data?.status === "ACTIVE"; - - const tools = isAuthenticated - ? await composio.tools.get(accountId, { - toolkits: [GOOGLE_SHEETS_TOOLKIT_SLUG], - }) - : { - google_sheets_login: googleSheetsLoginTool, - }; - - return tools; -} diff --git a/lib/agents/googleSheetsAgent/index.ts b/lib/agents/googleSheetsAgent/index.ts deleted file mode 100644 index 79bd8f91..00000000 --- a/lib/agents/googleSheetsAgent/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import getGoogleSheetsTools from "./getGoogleSheetsTools"; - -export { getGoogleSheetsTools }; diff --git a/lib/chat/__tests__/setupToolsForRequest.test.ts b/lib/chat/__tests__/setupToolsForRequest.test.ts index c2d36bb8..6d96cdef 100644 --- a/lib/chat/__tests__/setupToolsForRequest.test.ts +++ b/lib/chat/__tests__/setupToolsForRequest.test.ts @@ -6,17 +6,17 @@ vi.mock("@/lib/mcp/getMcpTools", () => ({ getMcpTools: vi.fn(), })); -vi.mock("@/lib/agents/googleSheetsAgent", () => ({ - getGoogleSheetsTools: vi.fn(), +vi.mock("@/lib/composio/toolRouter", () => ({ + getComposioTools: vi.fn(), })); // Import after mocks import { setupToolsForRequest } from "../setupToolsForRequest"; import { getMcpTools } from "@/lib/mcp/getMcpTools"; -import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; +import { getComposioTools } from "@/lib/composio/toolRouter"; const mockGetMcpTools = vi.mocked(getMcpTools); -const mockGetGoogleSheetsTools = vi.mocked(getGoogleSheetsTools); +const mockGetComposioTools = vi.mocked(getComposioTools); describe("setupToolsForRequest", () => { const mockMcpTools = { @@ -24,13 +24,9 @@ describe("setupToolsForRequest", () => { tool2: { description: "Tool 2", parameters: {} }, }; - const mockGoogleSheetsTools = { + const mockComposioTools = { + COMPOSIO_MANAGE_CONNECTIONS: { description: "Manage connections", parameters: {} }, googlesheets_create: { description: "Create sheet", parameters: {} }, - googlesheets_read: { description: "Read sheet", parameters: {} }, - }; - - const mockGoogleSheetsLoginTool = { - google_sheets_login: { description: "Login to Google Sheets", parameters: {} }, }; beforeEach(() => { @@ -39,8 +35,8 @@ describe("setupToolsForRequest", () => { // Default mock for MCP tools mockGetMcpTools.mockResolvedValue(mockMcpTools); - // Default mock for Google Sheets tools - returns login tool (not authenticated) - mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsLoginTool); + // Default mock for Composio tools + mockGetComposioTools.mockResolvedValue(mockComposioTools); }); describe("MCP tools integration", () => { @@ -84,23 +80,22 @@ describe("setupToolsForRequest", () => { }); }); - describe("Google Sheets tools integration", () => { - it("calls getGoogleSheetsTools with request body", async () => { + describe("Composio tools integration", () => { + it("calls getComposioTools with accountId and roomId", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, authToken: "test-token-123", + roomId: "room-456", messages: [{ id: "1", role: "user", content: "Create a spreadsheet" }], }; await setupToolsForRequest(body); - expect(mockGetGoogleSheetsTools).toHaveBeenCalledWith(body); + expect(mockGetComposioTools).toHaveBeenCalledWith("account-123", "room-456"); }); - it("includes Google Sheets tools when user is authenticated", async () => { - mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsTools); - + it("includes Composio tools in result", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -110,30 +105,13 @@ describe("setupToolsForRequest", () => { const result = await setupToolsForRequest(body); + expect(result).toHaveProperty("COMPOSIO_MANAGE_CONNECTIONS"); expect(result).toHaveProperty("googlesheets_create"); - expect(result).toHaveProperty("googlesheets_read"); - }); - - it("includes googleSheetsLoginTool when user is not authenticated", async () => { - mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsLoginTool); - - const body: ChatRequestBody = { - accountId: "account-123", - orgId: null, - authToken: "test-token-123", - messages: [{ id: "1", role: "user", content: "Create a spreadsheet" }], - }; - - const result = await setupToolsForRequest(body); - - expect(result).toHaveProperty("google_sheets_login"); }); }); describe("tool aggregation", () => { - it("merges MCP tools and Google Sheets tools", async () => { - mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsTools); - + it("merges MCP tools and Composio tools", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -143,19 +121,19 @@ describe("setupToolsForRequest", () => { const result = await setupToolsForRequest(body); - // Should have both MCP and Google Sheets tools + // Should have both MCP and Composio tools expect(result).toHaveProperty("tool1"); expect(result).toHaveProperty("tool2"); + expect(result).toHaveProperty("COMPOSIO_MANAGE_CONNECTIONS"); expect(result).toHaveProperty("googlesheets_create"); - expect(result).toHaveProperty("googlesheets_read"); }); - it("Google Sheets tools take precedence over MCP tools with same name", async () => { + it("Composio tools take precedence over MCP tools with same name", async () => { mockGetMcpTools.mockResolvedValue({ googlesheets_create: { description: "MCP version", parameters: {} }, }); - mockGetGoogleSheetsTools.mockResolvedValue({ + mockGetComposioTools.mockResolvedValue({ googlesheets_create: { description: "Composio version", parameters: {} }, }); @@ -168,7 +146,7 @@ describe("setupToolsForRequest", () => { const result = await setupToolsForRequest(body); - // Google Sheets (Composio) version should win + // Composio version should win expect(result.googlesheets_create).toEqual( expect.objectContaining({ description: "Composio version" }), ); @@ -192,8 +170,6 @@ describe("setupToolsForRequest", () => { }); it("excludes multiple tools", async () => { - mockGetGoogleSheetsTools.mockResolvedValue(mockGoogleSheetsTools); - const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -207,7 +183,7 @@ describe("setupToolsForRequest", () => { expect(result).not.toHaveProperty("tool1"); expect(result).not.toHaveProperty("googlesheets_create"); expect(result).toHaveProperty("tool2"); - expect(result).toHaveProperty("googlesheets_read"); + expect(result).toHaveProperty("COMPOSIO_MANAGE_CONNECTIONS"); }); it("returns all tools when excludeTools is undefined", async () => { diff --git a/lib/chat/setupToolsForRequest.ts b/lib/chat/setupToolsForRequest.ts index 15746300..b20261cc 100644 --- a/lib/chat/setupToolsForRequest.ts +++ b/lib/chat/setupToolsForRequest.ts @@ -1,31 +1,31 @@ import { ToolSet } from "ai"; import { filterExcludedTools } from "./filterExcludedTools"; import { ChatRequestBody } from "./validateChatRequest"; -import { getGoogleSheetsTools } from "@/lib/agents/googleSheetsAgent"; import { getMcpTools } from "@/lib/mcp/getMcpTools"; +import { getComposioTools } from "@/lib/composio/toolRouter"; /** * Sets up and filters tools for a chat request. * Aggregates tools from: * - MCP server (via HTTP transport to /api/mcp for proper auth) - * - Google Sheets (via Composio integration) + * - Composio Tool Router (Google Sheets and other connectors) * * @param body - The chat request body * @returns Filtered tool set ready for use */ export async function setupToolsForRequest(body: ChatRequestBody): Promise { - const { excludeTools, authToken } = body; + const { accountId, roomId, excludeTools, authToken } = body; // Only fetch MCP tools if we have an auth token - const mcpClientTools = authToken ? await getMcpTools(authToken) : {}; + const mcpTools = authToken ? await getMcpTools(authToken) : {}; - // Fetch Google Sheets tools (authenticated tools or login tool) - const googleSheetsTools = await getGoogleSheetsTools(body); + // Get Composio Tool Router tools (COMPOSIO_MANAGE_CONNECTIONS, etc.) + const composioTools = await getComposioTools(accountId, roomId); - // Merge all tools - Google Sheets tools take precedence over MCP tools + // Merge all tools const allTools: ToolSet = { - ...mcpClientTools, - ...googleSheetsTools, + ...mcpTools, + ...composioTools, }; const tools = filterExcludedTools(allTools, excludeTools); diff --git a/lib/composio/googleSheets/authenticateGoogleSheetsToolkit.ts b/lib/composio/googleSheets/authenticateGoogleSheetsToolkit.ts deleted file mode 100644 index 3236b060..00000000 --- a/lib/composio/googleSheets/authenticateGoogleSheetsToolkit.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CreateConnectedAccountOptions } from "@composio/core"; -import { getComposioClient } from "../client"; - -const googleSheetsAuthConfigId = process.env?.COMPOSIO_GOOGLE_SHEETS_AUTH_CONFIG_ID as string; -if (!googleSheetsAuthConfigId) { - throw new Error("COMPOSIO_GOOGLE_SHEETS_AUTH_CONFIG_ID not found in environment variables"); -} - -/** - * Authenticate a user with the Google Sheets toolkit. - * - * @param userId - The user ID to authenticate. - * @param options - The options for the authentication. - * @returns The connection request. - */ -async function authenticateGoogleSheetsToolkit( - userId: string, - options?: CreateConnectedAccountOptions, -) { - const composio = getComposioClient(); - const connectionRequest = await composio.connectedAccounts.initiate( - userId, - googleSheetsAuthConfigId, - options, - ); - return connectionRequest; -} - -export default authenticateGoogleSheetsToolkit; diff --git a/lib/composio/googleSheets/getConnectedAccount.ts b/lib/composio/googleSheets/getConnectedAccount.ts deleted file mode 100644 index f9847d5f..00000000 --- a/lib/composio/googleSheets/getConnectedAccount.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getComposioClient } from "@/lib/composio/client"; -import authenticateGoogleSheetsToolkit from "./authenticateGoogleSheetsToolkit"; -import { CreateConnectedAccountOptions } from "@composio/core"; - -export const GOOGLE_SHEETS_TOOLKIT_SLUG = "GOOGLESHEETS"; - -/** - * Get a connected account. - * - * @param accountId - The ID of the connected account to get. - * @param options - The options for the authentication. - * @returns The connected account. - */ -export default async function getConnectedAccount( - accountId: string, - options?: CreateConnectedAccountOptions, -) { - const composio = getComposioClient(); - let userAccounts = await composio.connectedAccounts.list({ - userIds: [accountId], - toolkitSlugs: [GOOGLE_SHEETS_TOOLKIT_SLUG], - }); - - if (userAccounts.items.length === 0) { - await authenticateGoogleSheetsToolkit(accountId, options); - userAccounts = await composio.connectedAccounts.list({ - userIds: [accountId], - toolkitSlugs: [GOOGLE_SHEETS_TOOLKIT_SLUG], - }); - } - - return userAccounts; -} diff --git a/lib/composio/googleSheets/refreshConnectedAccount.ts b/lib/composio/googleSheets/refreshConnectedAccount.ts deleted file mode 100644 index 91ab2130..00000000 --- a/lib/composio/googleSheets/refreshConnectedAccount.ts +++ /dev/null @@ -1,60 +0,0 @@ -import getConnectedAccount from "./getConnectedAccount"; -import { getComposioApiKey } from "../getComposioApiKey"; - -export interface ConnectedAccountRefreshResponse { - id: string; - redirect_url: string | null; - status: "INITIALIZING" | "INITIATED" | "ACTIVE" | "FAILED" | "EXPIRED" | "INACTIVE"; -} - -const COMPOSIO_API_BASE_URL = "https://backend.composio.dev"; - -/** - * Refresh a connected account. - * - * @param accountId - The ID of the connected account to refresh. - * @param redirectUrl - The URL to redirect to after the refresh. - * @returns The refreshed connected account. - */ -export default async function refreshConnectedAccount( - accountId: string, - redirectUrl?: string, -): Promise { - const apiKey = getComposioApiKey(); - const accounts = await getConnectedAccount(accountId); - - if (!accounts.items || accounts.items.length === 0) { - throw new Error("Connected account not found"); - } - - const connectedAccountId = accounts.items[0].id; - - if (!connectedAccountId) { - throw new Error("Connected account ID not found"); - } - - const url = new URL( - `${COMPOSIO_API_BASE_URL}/api/v3/connected_accounts/${connectedAccountId}/refresh`, - ); - - const body: { redirect_url?: string } = {}; - if (redirectUrl) { - body.redirect_url = redirectUrl; - } - - const response = await fetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": apiKey, - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to refresh connected account: ${response.status} ${errorText}`); - } - - return await response.json(); -} diff --git a/lib/composio/toolRouter/getTools.ts b/lib/composio/toolRouter/getTools.ts index 0140dd58..3d713428 100644 --- a/lib/composio/toolRouter/getTools.ts +++ b/lib/composio/toolRouter/getTools.ts @@ -15,9 +15,8 @@ const ALLOWED_TOOLS = [ /** * Runtime validation to check if an object is a valid Vercel AI SDK Tool. * - * Why: The Composio SDK's session.tools() returns a ToolSet (Record) - * from the configured provider. With VercelProvider, this returns Vercel AI SDK tools. - * We validate at runtime to ensure type safety before using bracket notation access. + * Composio SDK returns tools with { description, inputSchema, execute } + * Vercel AI SDK also accepts inputSchema as an alias for parameters. * * @param tool - The object to validate * @returns true if the object has required Tool properties @@ -29,12 +28,11 @@ function isValidTool(tool: unknown): tool is Tool { const obj = tool as Record; - // Vercel AI SDK Tool requires: description (optional), parameters, execute - // The execute function is what makes it callable + // Tool needs execute function and either parameters or inputSchema const hasExecute = typeof obj.execute === "function"; - const hasParameters = "parameters" in obj; + const hasSchema = "parameters" in obj || "inputSchema" in obj; - return hasExecute && hasParameters; + return hasExecute && hasSchema; } /** @@ -61,9 +59,7 @@ export async function getComposioTools( const filteredTools: ToolSet = {}; for (const toolName of ALLOWED_TOOLS) { - // Use Object.prototype.hasOwnProperty to safely check for property existence - // This handles both plain objects and class instances safely - if (Object.prototype.hasOwnProperty.call(allTools, toolName)) { + if (toolName in allTools) { const tool = (allTools as Record)[toolName]; if (isValidTool(tool)) { diff --git a/lib/composio/tools/googleSheetsLoginTool.ts b/lib/composio/tools/googleSheetsLoginTool.ts deleted file mode 100644 index c99dd7a5..00000000 --- a/lib/composio/tools/googleSheetsLoginTool.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; -import { tool } from "ai"; -import getConnectedAccount from "@/lib/composio/googleSheets/getConnectedAccount"; - -const schema = z.object({ - account_id: z - .string() - .min(1, "account_id is required and should be pulled from the system prompt."), -}); - -const googleSheetsLoginTool = tool({ - description: "Initiate the authentication flow for the Google Sheets account.", - inputSchema: schema, - execute: async ({ account_id }) => { - await getConnectedAccount(account_id); - return { - success: true, - message: - "Google Sheets login initiated successfully. Please click the button above to login with Google Sheets.", - }; - }, -}); - -export default googleSheetsLoginTool; diff --git a/lib/mcp/tools/composio/index.ts b/lib/mcp/tools/composio/index.ts deleted file mode 100644 index de072863..00000000 --- a/lib/mcp/tools/composio/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerComposioTools } from "./registerComposioTools"; - -/** - * Registers all Composio-related tools on the MCP server. - * - * Currently registers: - * - composio: Meta-tool for accessing Composio Tool Router - * - * @param server - The MCP server instance to register tools on. - */ -export function registerAllComposioTools(server: McpServer): void { - registerComposioTools(server); -} diff --git a/lib/mcp/tools/composio/registerComposioTools.ts b/lib/mcp/tools/composio/registerComposioTools.ts deleted file mode 100644 index 8c6bec3e..00000000 --- a/lib/mcp/tools/composio/registerComposioTools.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; -import type { McpAuthInfo } from "@/lib/mcp/verifyApiKey"; -import { resolveAccountId } from "@/lib/mcp/resolveAccountId"; -import { getComposioTools } from "@/lib/composio/toolRouter"; -import { getCallToolResult } from "@/lib/mcp/getCallToolResult"; - -const composioSchema = z.object({ - room_id: z.string().optional().describe("Chat room ID for OAuth redirect"), -}); - -type ComposioArgs = z.infer; - -export function registerComposioTools(server: McpServer): void { - server.registerTool( - "composio", - { - description: "Get Composio tools for Google Sheets integration.", - inputSchema: composioSchema, - }, - async (args: ComposioArgs, extra: RequestHandlerExtra) => { - const authInfo = extra.authInfo as McpAuthInfo | undefined; - const { accountId, error } = await resolveAccountId({ - authInfo, - accountIdOverride: undefined, - }); - - if (error || !accountId) { - return getCallToolResult(JSON.stringify({ error: error || "Authentication required" })); - } - - const tools = await getComposioTools(accountId, args.room_id); - return getCallToolResult(JSON.stringify(tools)); - } - ); -} diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index ca72d82a..0b60c5f0 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -16,20 +16,21 @@ import { registerCreateSegmentsTool } from "./registerCreateSegmentsTool"; import { registerAllYouTubeTools } from "./youtube"; import { registerTranscribeTools } from "./transcribe"; import { registerSendEmailTool } from "./registerSendEmailTool"; -import { registerAllComposioTools } from "./composio"; import { registerAllArtistTools } from "./artists"; /** * Registers all MCP tools on the server. * Add new tools here to automatically register them. * + * Note: Composio tools are added directly in setupToolsForRequest, + * not via MCP, because they are Vercel AI SDK tools from the Tool Router. + * * @param server - The MCP server instance to register tools on. */ export const registerAllTools = (server: McpServer): void => { registerAllArtistTools(server); registerAllArtistSocialsTools(server); registerAllCatalogTools(server); - registerAllComposioTools(server); registerAllFileTools(server); registerAllImageTools(server); registerAllSora2Tools(server);