diff --git a/apps/rowboat/app/actions/actions.ts b/apps/rowboat/app/actions/actions.ts index 4b765e85..c866c047 100644 --- a/apps/rowboat/app/actions/actions.ts +++ b/apps/rowboat/app/actions/actions.ts @@ -60,7 +60,6 @@ export async function scrapeWebpage(url: string): Promise, - projectTools: z.infer[], messages: z.infer[], ): Promise<{ streamId: string } | { billingError: string }> { await projectAuthCheck(projectId); @@ -83,6 +82,6 @@ export async function getAssistantResponseStreamId( return { billingError: error || 'Billing error' }; } - const response = await getAgenticResponseStreamId(projectId, workflow, projectTools, messages); + const response = await getAgenticResponseStreamId(projectId, workflow, messages); return response; } \ No newline at end of file diff --git a/apps/rowboat/app/actions/composio_actions.ts b/apps/rowboat/app/actions/composio_actions.ts index 7e60a105..cc2e9376 100644 --- a/apps/rowboat/app/actions/composio_actions.ts +++ b/apps/rowboat/app/actions/composio_actions.ts @@ -3,9 +3,6 @@ import { z } from "zod"; import { listToolkits as libListToolkits, listTools as libListTools, - searchTools as libSearchTools, - getToolsByIds as libGetToolsByIds, - getTool as libGetTool, getConnectedAccount as libGetConnectedAccount, deleteConnectedAccount as libDeleteConnectedAccount, listAuthConfigs as libListAuthConfigs, @@ -23,7 +20,6 @@ import { ZCredentials, } from "@/app/lib/composio/composio"; import { ComposioConnectedAccount } from "@/app/lib/types/project_types"; -import { WorkflowTool } from "@/app/lib/types/workflow_types"; import { getProjectConfig, projectAuthCheck } from "./project_actions"; import { projectsCollection } from "../lib/mongodb"; @@ -46,29 +42,11 @@ export async function getToolkit(projectId: string, toolkitSlug: string): Promis return await libGetToolkit(toolkitSlug); } -export async function listTools(projectId: string, toolkitSlug: string, cursor: string | null = null): Promise>>> { +export async function listTools(projectId: string, toolkitSlug: string, searchQuery: string | null, cursor: string | null = null): Promise>>> { await projectAuthCheck(projectId); - return await libListTools(toolkitSlug, cursor); + return await libListTools(toolkitSlug, searchQuery, cursor); } -// New efficient search functions - -export async function searchTools(projectId: string, searchQuery: string, cursor: string | null = null, limit?: number): Promise>>> { - await projectAuthCheck(projectId); - return await libSearchTools(searchQuery, cursor, limit); -} - -export async function getToolsByIds(projectId: string, toolSlugs: string[], cursor: string | null = null): Promise>>> { - await projectAuthCheck(projectId); - return await libGetToolsByIds(toolSlugs, cursor); -} - -export async function getTool(projectId: string, toolSlug: string): Promise> { - await projectAuthCheck(projectId); - return await libGetTool(toolSlug); -} - - export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise> { await projectAuthCheck(projectId); @@ -237,196 +215,5 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str const key = `composioConnectedAccounts.${toolkitSlug}`; await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } }); - // Notify other tabs about the tools update (lightweight refresh) - if (typeof window !== 'undefined') { - localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); - } - return true; -} - -// Note: composio tools are now stored in workflow.tools array with isComposio: true -// This function provides backward compatibility by updating workflow tools -export async function getComposioToolsFromWorkflow(projectId: string): Promise[]> { - await projectAuthCheck(projectId); - - // Get the project to access draft workflow - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project || !project.draftWorkflow) { - return []; - } - - // Extract composio tools from workflow and convert back to ZTool format - const composioTools = project.draftWorkflow.tools - .filter(tool => tool.isComposio && tool.composioData) - .map(tool => ({ - slug: tool.composioData!.slug, - name: tool.name, - description: tool.description, - no_auth: tool.composioData!.noAuth, - input_parameters: { - type: 'object' as const, - properties: tool.parameters.properties, - required: tool.parameters.required || [] - }, - toolkit: { - name: tool.composioData!.toolkitName, - slug: tool.composioData!.toolkitSlug, - logo: tool.composioData!.logo, - } - })); - - return composioTools; -} - -export async function updateComposioSelectedTools(projectId: string, tools: z.infer[]): Promise { - await projectAuthCheck(projectId); - - // Get the project to access draft workflow - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project || !project.draftWorkflow) { - throw new Error(`Project ${projectId} not found or has no draft workflow`); - } - - // Convert Composio tools to workflow tool format - const composioWorkflowTools: z.infer[] = tools.map(tool => ({ - name: tool.slug, - description: tool.description || "", - parameters: { - type: 'object' as const, - properties: tool.input_parameters?.properties || {}, - required: tool.input_parameters?.required || [] - }, - isComposio: true, - composioData: { - slug: tool.slug, - noAuth: tool.no_auth, - toolkitName: tool.toolkit.name, - toolkitSlug: tool.toolkit.slug, - logo: tool.toolkit.logo, - }, - })); - - // Remove existing composio tools and add new ones - const nonComposioTools = project.draftWorkflow.tools.filter(tool => !tool.isComposio); - const updatedWorkflow = { - ...project.draftWorkflow, - tools: [...nonComposioTools, ...composioWorkflowTools], - lastUpdatedAt: new Date().toISOString() - }; - - // Update the project's draft workflow - const result = await projectsCollection.updateOne( - { _id: projectId }, - { $set: { draftWorkflow: updatedWorkflow } } - ); - - if (result.modifiedCount === 0) { - throw new Error(`Failed to update workflow for project ${projectId}`); - } - - // Notify other tabs about the tools update (lightweight refresh) - if (typeof window !== 'undefined') { - localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); - } -} - -// Note: composio mock states are now stored in workflow.composioMockToolkitStates -// This function provides backward compatibility by updating workflow mock states -export async function toggleMockToolkitState(projectId: string, toolkitSlug: string, isMocked: boolean, mockInstructions?: string): Promise { - await projectAuthCheck(projectId); - - // Get the project to access draft workflow - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project || !project.draftWorkflow) { - throw new Error(`Project ${projectId} not found or has no draft workflow`); - } - - const now = new Date().toISOString(); - let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) }; - - if (isMocked) { - // Enable mock mode - updatedMockToolkitStates[toolkitSlug] = { - toolkitSlug, - isMocked: true, - mockInstructions: mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.', - autoSubmitMockedResponse: false, - createdAt: now, - lastUpdatedAt: now, - }; - } else { - // Disable mock mode - remove the toolkit from the object - delete updatedMockToolkitStates[toolkitSlug]; - } - - // Update the workflow with new mock states - const updatedWorkflow = { - ...project.draftWorkflow, - composioMockToolkitStates: updatedMockToolkitStates, - lastUpdatedAt: now - }; - - // Update the project's draft workflow - const result = await projectsCollection.updateOne( - { _id: projectId }, - { $set: { draftWorkflow: updatedWorkflow } } - ); - - if (result.modifiedCount === 0) { - throw new Error(`Failed to update workflow mock states for project ${projectId}`); - } - - // Notify other tabs about the tools update (lightweight refresh) - if (typeof window !== 'undefined') { - localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); - } -} - -// Note: composio mock states are now stored in workflow.composioMockToolkitStates -// This function provides backward compatibility by updating workflow mock states -export async function updateMockToolkitInstructions(projectId: string, toolkitSlug: string, mockInstructions: string): Promise { - await projectAuthCheck(projectId); - - // Get the project to access draft workflow - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project || !project.draftWorkflow) { - throw new Error(`Project ${projectId} not found or has no draft workflow`); - } - - const now = new Date().toISOString(); - let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) }; - - // Update the mock instructions for the specified toolkit - if (updatedMockToolkitStates[toolkitSlug]) { - updatedMockToolkitStates[toolkitSlug] = { - ...updatedMockToolkitStates[toolkitSlug], - mockInstructions, - lastUpdatedAt: now - }; - - // Update the workflow with new mock states - const updatedWorkflow = { - ...project.draftWorkflow, - composioMockToolkitStates: updatedMockToolkitStates, - lastUpdatedAt: now - }; - - // Update the project's draft workflow - const result = await projectsCollection.updateOne( - { _id: projectId }, - { $set: { draftWorkflow: updatedWorkflow } } - ); - - if (result.modifiedCount === 0) { - throw new Error(`Failed to update workflow mock instructions for project ${projectId}`); - } - - // Notify other tabs about the tools update - if (typeof window !== 'undefined') { - localStorage.setItem(`tools-updated-${projectId}`, Date.now().toString()); - } - } else { - throw new Error(`Mock toolkit state for ${toolkitSlug} not found in project ${projectId}`); - } } \ No newline at end of file diff --git a/apps/rowboat/app/actions/copilot_actions.ts b/apps/rowboat/app/actions/copilot_actions.ts index f2df813e..84bd0828 100644 --- a/apps/rowboat/app/actions/copilot_actions.ts +++ b/apps/rowboat/app/actions/copilot_actions.ts @@ -11,8 +11,6 @@ import { check_query_limit } from "../lib/rate_limiting"; import { QueryLimitError } from "../lib/client_utils"; import { projectAuthCheck } from "./project_actions"; import { redisClient } from "../lib/redis"; -import { collectProjectTools } from "../lib/project_tools"; -import { mergeProjectTools } from "../lib/types/project_types"; import { authorizeUserAction, logUsage } from "./billing_actions"; import { USE_BILLING } from "../lib/feature_flags"; import { WithStringId } from "../lib/types/types"; @@ -44,21 +42,12 @@ export async function getCopilotResponseStream( if (!await check_query_limit(projectId)) { throw new QueryLimitError(); } - - // Get MCP tools from project and merge with workflow tools - const projectTools = await collectProjectTools(projectId); - // Convert workflow to copilot format with both workflow and project tools - const wflow = { - ...current_workflow_config, - tools: mergeProjectTools(current_workflow_config.tools, projectTools) - }; - // prepare request const request: z.infer = { projectId, messages, - workflow: wflow, + workflow: current_workflow_config, context, dataSources: dataSources, }; @@ -97,20 +86,11 @@ export async function getCopilotAgentInstructions( return { billingError: authResponse.error || 'Billing error' }; } - // Get MCP tools from project and merge with workflow tools - const projectTools = await collectProjectTools(projectId); - - // Convert workflow to copilot format with both workflow and project tools - const wflow = { - ...current_workflow_config, - tools: mergeProjectTools(current_workflow_config.tools, projectTools) - }; - // prepare request const request: z.infer = { projectId, messages, - workflow: wflow, + workflow: current_workflow_config, context: { type: 'agent', name: agentName, diff --git a/apps/rowboat/app/actions/custom_mcp_server_actions.ts b/apps/rowboat/app/actions/custom_mcp_server_actions.ts new file mode 100644 index 00000000..a678dec9 --- /dev/null +++ b/apps/rowboat/app/actions/custom_mcp_server_actions.ts @@ -0,0 +1,67 @@ +'use server'; + +import { projectsCollection } from '../lib/mongodb'; +import { z } from 'zod'; +import { projectAuthCheck } from './project_actions'; +import { CustomMcpServer } from '../lib/types/project_types'; +import { getMcpClient } from '../lib/mcp'; +import { WorkflowTool } from '../lib/types/workflow_types'; +import { authCheck } from './auth_actions'; + +type McpServerType = z.infer; + +function validateUrl(url: string): string { + try { + const parsedUrl = new URL(url); + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new Error('Invalid protocol'); + } + return parsedUrl.toString(); + } catch (error) { + throw new Error('Invalid URL'); + } +} + +export async function addServer(projectId: string, name: string, server: McpServerType): Promise { + await projectAuthCheck(projectId); + + // Validate the server URL + validateUrl(server.serverUrl); + + // Update the customMcpServers record with the server + await projectsCollection.updateOne( + { _id: projectId }, + { $set: { [`customMcpServers.${name}`]: server } } + ); +} + +export async function removeServer(projectId: string, name: string): Promise { + await projectAuthCheck(projectId); + + await projectsCollection.updateOne( + { _id: projectId }, + { $unset: { [`customMcpServers.${name}`]: "" } } + ); +} + +export async function fetchTools(serverUrl: string, serverName: string): Promise[]> { + await authCheck(); + + const client = await getMcpClient(serverUrl, serverName); + const result = await client.listTools(); + return result.tools.map(tool => { + return { + name: tool.name, + description: tool.description || '', + parameters: { + type: 'object', + properties: tool.inputSchema?.properties || {}, + required: tool.inputSchema?.required || [], + additionalProperties: true, + }, + isMcp: true, + mcpServerName: serverName, + mcpServerURL: serverUrl, + }; + }); +} diff --git a/apps/rowboat/app/actions/custom_server_actions.ts b/apps/rowboat/app/actions/custom_server_actions.ts deleted file mode 100644 index cb7a82c7..00000000 --- a/apps/rowboat/app/actions/custom_server_actions.ts +++ /dev/null @@ -1,93 +0,0 @@ -'use server'; - -import { projectsCollection } from '../lib/mongodb'; -import { MCPServer } from '../lib/types/types'; -import { z } from 'zod'; -import { projectAuthCheck } from './project_actions'; - -type McpServerType = z.infer; - -function formatServerUrl(url: string): string { - // Ensure URL starts with http:// or https:// - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'http://' + url; - } - // Remove trailing slash if present - return url.replace(/\/$/, ''); -} - -export async function fetchCustomServers(projectId: string) { - await projectAuthCheck(projectId); - - const project = await projectsCollection.findOne({ _id: projectId }); - return (project?.mcpServers || []) - .filter(server => server.serverType === 'custom') - .map(server => ({ - ...server, - serverType: 'custom' as const, - isReady: true // Custom servers are always ready - })); -} - -export async function addCustomServer(projectId: string, server: McpServerType) { - await projectAuthCheck(projectId); - - // Format the server URL and ensure isReady is true for custom servers - const formattedServer = { - ...server, - serverUrl: formatServerUrl(server.serverUrl || ''), - isReady: true // Custom servers are always ready - }; - - await projectsCollection.updateOne( - { _id: projectId }, - { $push: { mcpServers: formattedServer } } - ); - - return formattedServer; -} - -export async function removeCustomServer(projectId: string, serverName: string) { - await projectAuthCheck(projectId); - - await projectsCollection.updateOne( - { _id: projectId }, - { $pull: { mcpServers: { name: serverName } } } - ); -} - -export async function toggleCustomServer(projectId: string, serverName: string, isActive: boolean) { - await projectAuthCheck(projectId); - - await projectsCollection.updateOne( - { _id: projectId, "mcpServers.name": serverName }, - { - $set: { - "mcpServers.$.isActive": isActive, - "mcpServers.$.isReady": isActive // Update isReady along with isActive - } - } - ); -} - -export async function updateCustomServerTools( - projectId: string, - serverName: string, - tools: McpServerType['tools'], - availableTools?: McpServerType['availableTools'] -) { - await projectAuthCheck(projectId); - - const update: Record = { - "mcpServers.$.tools": tools - }; - - if (availableTools) { - update["mcpServers.$.availableTools"] = availableTools; - } - - await projectsCollection.updateOne( - { _id: projectId, "mcpServers.name": serverName }, - { $set: update } - ); -} \ No newline at end of file diff --git a/apps/rowboat/app/actions/project_actions.ts b/apps/rowboat/app/actions/project_actions.ts index d9809494..0300c91c 100644 --- a/apps/rowboat/app/actions/project_actions.ts +++ b/apps/rowboat/app/actions/project_actions.ts @@ -14,15 +14,6 @@ import { USE_AUTH } from "../lib/feature_flags"; import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions"; import { authorizeUserAction } from "./billing_actions"; import { Workflow } from "../lib/types/workflow_types"; -import { WorkflowTool } from "../lib/types/workflow_types"; -import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools"; -import { - searchTools as libSearchTools, - getToolsByIds as libGetToolsByIds, - getTool as libGetTool, - ZTool, - ZToolkit -} from "../lib/composio/composio"; const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || ''; @@ -313,113 +304,6 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id: return { id: projectId }; } -async function detectAndAddComposioTools(projectId: string, workflow: z.infer) { - // Extract tool mentions from agent instructions - const toolMentionPattern = /\[@tool:([^\]]+)\]\(#mention[^\)]*\)/g; - const mentionedToolNames = new Set(); - - // Scan all agent instructions for tool mentions - for (const agent of workflow.agents || []) { - const instructions = agent.instructions || ""; - let match: RegExpExecArray | null; - while ((match = toolMentionPattern.exec(instructions))) { - mentionedToolNames.add(match[1]); - } - } - - if (mentionedToolNames.size === 0) { - return; // No tool mentions found - } - - console.log(`Found ${mentionedToolNames.size} tool mentions in workflow:`, Array.from(mentionedToolNames)); - - // Search for these tools in Composio using the new efficient search methods - const foundTools: z.infer[] = []; - - try { - // Method 1: Try to get tools directly by their exact slugs/names - const mentionedToolNamesArray = Array.from(mentionedToolNames); - - try { - const directToolsResponse = await libGetToolsByIds(mentionedToolNamesArray); - foundTools.push(...directToolsResponse.items); - console.log(`Found ${directToolsResponse.items.length} tools by direct lookup`); - } catch (error) { - console.log('Direct tool lookup failed, trying search approach'); - } - - // Method 2: For any remaining tools, use search functionality - const foundToolSlugs = new Set(foundTools.map(tool => tool.slug)); - const foundToolNames = new Set(foundTools.map(tool => tool.name)); - const remainingToolNames = mentionedToolNamesArray.filter(name => - !foundToolSlugs.has(name) && !foundToolNames.has(name) - ); - - for (const toolName of remainingToolNames) { - try { - // Search for tools by name/description - const searchResponse = await libSearchTools(toolName, null, 10); - - // Find exact matches by name or slug - const exactMatches = searchResponse.items.filter(tool => - tool.name === toolName || - tool.slug === toolName || - tool.name.toLowerCase() === toolName.toLowerCase() || - tool.slug.toLowerCase() === toolName.toLowerCase() - ); - - if (exactMatches.length > 0) { - foundTools.push(...exactMatches); - console.log(`Found ${exactMatches.length} tools for search term "${toolName}"`); - } else { - console.log(`No exact matches found for tool "${toolName}"`); - } - } catch (error) { - console.error(`Error searching for tool "${toolName}":`, error); - } - } - - } catch (error) { - console.error('Error searching for Composio tools:', error); - return; - } - - if (foundTools.length > 0) { - console.log(`Adding ${foundTools.length} Composio tools to workflow`); - - // Remove duplicates based on slug - const uniqueTools = foundTools.filter((tool, index, self) => - index === self.findIndex(t => t.slug === tool.slug) - ); - - // Convert Composio tools to workflow tool format - const composioWorkflowTools: z.infer[] = uniqueTools.map(tool => ({ - name: tool.slug, - description: tool.description || "", - parameters: { - type: 'object' as const, - properties: tool.input_parameters?.properties || {}, - required: tool.input_parameters?.required || [] - }, - isComposio: true, - composioData: { - slug: tool.slug, - noAuth: tool.no_auth, - toolkitName: tool.toolkit.name, - toolkitSlug: tool.toolkit.slug, - logo: tool.toolkit.logo, - }, - })); - - // Add these tools to the workflow.tools array - workflow.tools = [...workflow.tools, ...composioWorkflowTools]; - - console.log(`Added ${composioWorkflowTools.length} Composio tools to workflow`); - } else { - console.log('No matching Composio tools found for the mentioned tool names'); - } -} - export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> { const user = await authCheck(); const workflowJson = formData.get('workflowJson') as string; @@ -441,23 +325,9 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise return response; } const projectId = response.id; - - // Automatically detect and add Composio tools mentioned in agent instructions - try { - await detectAndAddComposioTools(projectId, workflow); - } catch (error) { - // Log error but don't fail the import if tool detection fails - console.error('Failed to auto-detect Composio tools:', error); - } - return { id: projectId }; } -export async function collectProjectTools(projectId: string): Promise[]> { - await projectAuthCheck(projectId); - return libCollectProjectTools(projectId); -} - export async function saveWorkflow(projectId: string, workflow: z.infer) { await projectAuthCheck(projectId); diff --git a/apps/rowboat/app/api/stream-response/[streamId]/route.ts b/apps/rowboat/app/api/stream-response/[streamId]/route.ts index 14d09a3d..743acf53 100644 --- a/apps/rowboat/app/api/stream-response/[streamId]/route.ts +++ b/apps/rowboat/app/api/stream-response/[streamId]/route.ts @@ -13,7 +13,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: } // parse the payload - const { projectId, workflow, projectTools, messages } = ZStreamAgentResponsePayload.parse(JSON.parse(payload)); + const { projectId, workflow, messages } = ZStreamAgentResponsePayload.parse(JSON.parse(payload)); console.log('payload', payload); // fetch billing customer id @@ -29,7 +29,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: async start(controller) { try { // Iterate over the generator - for await (const event of streamResponse(projectId, workflow, projectTools, messages)) { + for await (const event of streamResponse(projectId, workflow, messages)) { // Check if this is a message event (has role property) if ('role' in event) { if (event.role === 'assistant') { diff --git a/apps/rowboat/app/api/twilio/inbound_call/route.ts b/apps/rowboat/app/api/twilio/inbound_call/route.ts index 1869a954..7fb2a886 100644 --- a/apps/rowboat/app/api/twilio/inbound_call/route.ts +++ b/apps/rowboat/app/api/twilio/inbound_call/route.ts @@ -1,6 +1,5 @@ import { getResponse } from "@/app/lib/agents"; import { projectsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; -import { collectProjectTools } from "@/app/lib/project_tools"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; import { z } from "zod"; @@ -78,12 +77,9 @@ export async function POST(request: Request) { return reject('rejected'); } - // fetch project tools - const projectTools = await collectProjectTools(projectId); - // this is the first turn, get the initial assistant response // and validate it - const { messages } = await getResponse(projectId, workflow, projectTools, []); + const { messages } = await getResponse(projectId, workflow, []); if (messages.length === 0) { logger.log('Agent response is empty'); return hangup(); diff --git a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts index 29fa71dd..56e60b0c 100644 --- a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts +++ b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts @@ -1,6 +1,5 @@ import { getResponse } from "@/app/lib/agents"; import { projectsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; -import { collectProjectTools } from "@/app/lib/project_tools"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; import { z } from "zod"; @@ -50,9 +49,6 @@ export async function POST( return hangup(); } - // fetch project tools - const projectTools = await collectProjectTools(projectId); - // add user speech as user message, and get assistant response const reqMessages: z.infer[] = [ ...call.messages, @@ -61,7 +57,7 @@ export async function POST( content: data.SpeechResult, } ]; - const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages); + const { messages } = await getResponse(projectId, workflow, reqMessages); if (messages.length === 0) { logger.log('Agent response is empty'); return hangup(); diff --git a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts index 1001cf9c..8008fae3 100644 --- a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts +++ b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts @@ -6,7 +6,6 @@ import { authCheck } from "../../utils"; import { ApiRequest, ApiResponse } from "../../../../lib/types/types"; import { check_query_limit } from "../../../../lib/rate_limiting"; import { PrefixLogger } from "../../../../lib/utils"; -import { collectProjectTools } from "@/app/lib/project_tools"; import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing"; import { USE_BILLING } from "@/app/lib/feature_flags"; import { getResponse } from "@/app/lib/agents"; @@ -61,9 +60,6 @@ export async function POST( return Response.json({ error: "Project not found" }, { status: 404 }); } - // fetch project tools - const projectTools = await collectProjectTools(projectId); - // fetch workflow const workflow = project.liveWorkflow; if (!workflow) { @@ -94,7 +90,7 @@ export async function POST( } // get assistant response - const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages); + const { messages } = await getResponse(projectId, workflow, reqMessages); // log billing usage if (USE_BILLING && billingCustomerId) { diff --git a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts index 7f901e70..e0101da0 100644 --- a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts +++ b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts @@ -6,7 +6,6 @@ import { ObjectId, WithId } from "mongodb"; import { authCheck } from "../../../utils"; import { check_query_limit } from "../../../../../../lib/rate_limiting"; import { PrefixLogger } from "../../../../../../lib/utils"; -import { collectProjectTools } from "@/app/lib/project_tools"; import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing"; import { USE_BILLING } from "@/app/lib/feature_flags"; import { getResponse } from "@/app/lib/agents"; @@ -181,9 +180,6 @@ export async function POST( throw new Error("Project settings not found"); } - // fetch project tools - const projectTools = await collectProjectTools(session.projectId); - // fetch workflow const workflow = projectSettings.liveWorkflow; if (!workflow) { @@ -211,7 +207,7 @@ export async function POST( const inMessages: z.infer[] = convert(messages); inMessages.push(userMessage); - const { messages: responseMessages } = await getResponse(session.projectId, workflow, projectTools, [systemMessage, ...inMessages]); + const { messages: responseMessages } = await getResponse(session.projectId, workflow, [systemMessage, ...inMessages]); const convertedResponseMessages = convertBack(responseMessages); const unsavedMessages = [ userMessage, diff --git a/apps/rowboat/app/lib/agents.ts b/apps/rowboat/app/lib/agents.ts index 618a5e3e..60b2ae09 100644 --- a/apps/rowboat/app/lib/agents.ts +++ b/apps/rowboat/app/lib/agents.ts @@ -17,7 +17,6 @@ import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } f import { qdrantClient } from '../lib/qdrant'; import { EmbeddingRecord } from "./types/datasource_types"; import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types"; -import { Project } from "./types/project_types"; import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions"; import { PrefixLogger } from "./utils"; import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types"; @@ -254,17 +253,27 @@ async function invokeMcpTool( projectId: string, name: string, input: any, - mcpServerURL: string, mcpServerName: string ) { logger = logger.child(`invokeMcpTool`); logger.log(`projectId: ${projectId}`); logger.log(`name: ${name}`); logger.log(`input: ${JSON.stringify(input)}`); - logger.log(`mcpServerURL: ${mcpServerURL}`); logger.log(`mcpServerName: ${mcpServerName}`); - const client = await getMcpClient(mcpServerURL, mcpServerName || ''); + // Get project configuration + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project) { + throw new Error(`project ${projectId} not found`); + } + + // get server url from project data + const mcpServerURL = project.customMcpServers?.[mcpServerName]?.serverUrl; + if (!mcpServerURL) { + throw new Error(`mcp server url not found for project ${projectId} and server ${mcpServerName}`); + } + + const client = await getMcpClient(mcpServerURL, mcpServerName); const result = await client.callTool({ name, arguments: input, @@ -281,8 +290,6 @@ async function invokeComposioTool( name: string, composioData: z.infer['composioData'] & {}, input: any, - workflow: z.infer, - toolDescription?: string, ) { logger = logger.child(`invokeComposioTool`); logger.log(`projectId: ${projectId}`); @@ -291,36 +298,12 @@ async function invokeComposioTool( const { slug, toolkitSlug, noAuth } = composioData; - // Get project configuration to check for connected accounts (still stored in project) - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) { - throw new Error(`project ${projectId} not found`); - } - - // Check if toolkit is in mock mode (now from workflow) - const mockState = workflow.composioMockToolkitStates?.[toolkitSlug]; - if (mockState?.isMocked) { - logger.log(`toolkit ${toolkitSlug} is in mock mode, using mock response`); - - // Use the existing invokeMockTool function to generate a mock response - const mockInstructions = mockState.mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.'; - const description = toolDescription || `${name} tool from ${toolkitSlug} toolkit`; - - const mockResponse = await invokeMockTool( - logger, - name, - JSON.stringify(input), - description, - mockInstructions - ); - - logger.log(`mock tool result: ${mockResponse}`); - return mockResponse; - } - - // Normal execution path - check for authentication let connectedAccountId: string | undefined = undefined; if (!noAuth) { + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project) { + throw new Error(`project ${projectId} not found`); + } connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id; if (!connectedAccountId) { throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`); @@ -447,7 +430,7 @@ function createMcpTool( config: z.infer, projectId: string ): Tool { - const { name, description, parameters, mcpServerName, mcpServerURL } = config; + const { name, description, parameters, mcpServerName } = config; return tool({ name, @@ -461,7 +444,7 @@ function createMcpTool( }, async execute(input: any) { try { - const result = await invokeMcpTool(logger, projectId, name, input, mcpServerURL || '', mcpServerName || ''); + const result = await invokeMcpTool(logger, projectId, name, input, mcpServerName || ''); return JSON.stringify({ result, }); @@ -479,8 +462,7 @@ function createMcpTool( function createComposioTool( logger: PrefixLogger, config: z.infer, - projectId: string, - workflow: z.infer + projectId: string ): Tool { const { name, description, parameters, composioData } = config; @@ -500,7 +482,7 @@ function createComposioTool( }, async execute(input: any) { try { - const result = await invokeComposioTool(logger, projectId, name, composioData, input, workflow, description); + const result = await invokeComposioTool(logger, projectId, name, composioData, input); return JSON.stringify({ result, }); @@ -520,7 +502,6 @@ function createAgent( projectId: string, config: z.infer, tools: Record, - projectTools: z.infer[], workflow: z.infer, promptConfig: Record>, ): { agent: Agent, entities: z.infer[] } { @@ -550,7 +531,7 @@ ${'-'.repeat(100)} ${CHILD_TRANSFER_RELATED_INSTRUCTIONS} `; - let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow, projectTools); + let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow); agentLogger.log(`instructions: ${JSON.stringify(sanitized)}`); agentLogger.log(`mentions: ${JSON.stringify(entities)}`); @@ -783,7 +764,7 @@ Basic context: } } -function mapConfig(workflow: z.infer, projectTools: z.infer[]): { +function mapConfig(workflow: z.infer): { agentConfig: Record>; toolConfig: Record>; promptConfig: Record>; @@ -792,10 +773,7 @@ function mapConfig(workflow: z.infer, projectTools: z.infer> = [ - ...workflow.tools, - ...projectTools, - ].reduce((acc, tool) => ({ + const toolConfig: Record> = workflow.tools.reduce((acc, tool) => ({ ...acc, [tool.name]: tool }), {}); @@ -837,15 +815,15 @@ function createTools( mockInstructions: workflow.mockTools?.[toolName], // override mock instructions }); logger.log(`created mock tool: ${toolName}`); + } else if (config.mockTool) { + tools[toolName] = createMockTool(logger, config); + logger.log(`created mock tool: ${toolName}`); } else if (config.isMcp) { tools[toolName] = createMcpTool(logger, config, projectId); logger.log(`created mcp tool: ${toolName}`); } else if (config.isComposio) { - tools[toolName] = createComposioTool(logger, config, projectId, workflow); + tools[toolName] = createComposioTool(logger, config, projectId); logger.log(`created composio tool: ${toolName}`); - } else if (config.mockTool) { - tools[toolName] = createMockTool(logger, config); - logger.log(`created mock tool: ${toolName}`); } else { tools[toolName] = createWebhookTool(logger, config, projectId); logger.log(`created webhook tool: ${toolName}`); @@ -860,7 +838,6 @@ function createAgents( workflow: z.infer, agentConfig: Record>, tools: Record, - projectTools: z.infer[], promptConfig: Record>, ): { agents: Record, mentions: Record[]>, originalInstructions: Record, originalHandoffs: Record } { const agents: Record = {}; @@ -875,7 +852,6 @@ function createAgents( projectId, config, tools, - projectTools, workflow, promptConfig, ); @@ -956,7 +932,6 @@ function maybeInjectGiveUpControlInstructions( export async function* streamResponse( projectId: string, workflow: z.infer, - projectTools: z.infer[], messages: z.infer[], ): AsyncIterable | z.infer> { // Divider log for tracking agent loop start @@ -975,7 +950,7 @@ export async function* streamResponse( } // create map of agent, tool and prompt configs - const { agentConfig, toolConfig, promptConfig } = mapConfig(workflow, projectTools); + const { agentConfig, toolConfig, promptConfig } = mapConfig(workflow); const stack: string[] = []; @@ -985,7 +960,7 @@ export async function* streamResponse( const tools = createTools(logger, projectId, workflow, toolConfig); // create agents - const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, projectTools, promptConfig); + const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, promptConfig); // track agent to agent calls const transferCounter = new AgentTransferCounter(); @@ -1241,7 +1216,6 @@ export async function* streamResponse( export async function getResponse( projectId: string, workflow: z.infer, - projectTools: z.infer[], messages: z.infer[], ): Promise<{ messages: z.infer[], @@ -1255,7 +1229,7 @@ export async function getResponse( completion: 0, }, }; - for await (const event of streamResponse(projectId, workflow, projectTools, messages)) { + for await (const event of streamResponse(projectId, workflow, messages)) { if ('role' in event) { out.push(event); } diff --git a/apps/rowboat/app/lib/composio/composio.ts b/apps/rowboat/app/lib/composio/composio.ts index 64c07679..2515fe45 100644 --- a/apps/rowboat/app/lib/composio/composio.ts +++ b/apps/rowboat/app/lib/composio/composio.ts @@ -278,11 +278,14 @@ export async function getToolkit(toolkitSlug: string): Promise>>> { +export async function listTools(toolkitSlug: string, searchQuery: string | null = null, cursor: string | null = null): Promise>>> { const url = new URL(`${BASE_URL}/tools`); // set params url.searchParams.set("toolkit_slug", toolkitSlug); + if (searchQuery) { + url.searchParams.set("search", searchQuery); + } if (cursor) { url.searchParams.set("cursor", cursor); } @@ -291,39 +294,6 @@ export async function listTools(toolkitSlug: string, cursor: string | null = nul return composioApiCall(ZListResponse(ZTool), url.toString()); } -export async function searchTools(searchQuery: string, cursor: string | null = null, limit: number = 50): Promise>>> { - const url = new URL(`${BASE_URL}/tools`); - - // set params - url.searchParams.set("search", searchQuery); - if (cursor) { - url.searchParams.set("cursor", cursor); - } - url.searchParams.set("limit", limit.toString()); - - // fetch - return composioApiCall(ZListResponse(ZTool), url.toString()); -} - -export async function getToolsByIds(toolSlugs: string[], cursor: string | null = null): Promise>>> { - const url = new URL(`${BASE_URL}/tools`); - - // set params - pass tool slugs as comma-separated string - url.searchParams.set("tool_slugs", toolSlugs.join(",")); - if (cursor) { - url.searchParams.set("cursor", cursor); - } - - // fetch - return composioApiCall(ZListResponse(ZTool), url.toString()); -} - -export async function getTool(toolSlug: string): Promise> { - const url = new URL(`${BASE_URL}/tools/${toolSlug}`); - return composioApiCall(ZTool, url.toString()); -} - - export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise>>> { const url = new URL(`${BASE_URL}/auth_configs`); url.searchParams.set("toolkit_slug", toolkitSlug); diff --git a/apps/rowboat/app/lib/project_tools.ts b/apps/rowboat/app/lib/project_tools.ts deleted file mode 100644 index 7d8fb8f5..00000000 --- a/apps/rowboat/app/lib/project_tools.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod"; -import { projectsCollection } from "./mongodb"; -import { WorkflowTool } from "./types/workflow_types"; - -export async function collectProjectTools(projectId: string): Promise[]> { - const tools: z.infer[] = []; - - // Get project data - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) { - throw new Error(`Project ${projectId} not found`); - } - - // Convert MCP tools to workflow tools format, but only from ready servers - if (project.mcpServers) { - for (const server of project.mcpServers) { - if (server.isReady) { - for (const tool of server.tools) { - tools.push({ - name: tool.name, - description: tool.description || "", - parameters: { - type: 'object' as const, - properties: tool.parameters?.properties || {}, - required: tool.parameters?.required || [] - }, - isMcp: true, - mcpServerName: server.name, - mcpServerURL: server.serverUrl, - }); - } - } - } - } - - // Note: Composio tools are now stored in workflow.tools array with isComposio: true - // This function now only collects MCP tools since composio tools are managed in workflow - - return tools; -} diff --git a/apps/rowboat/app/lib/types/project_types.ts b/apps/rowboat/app/lib/types/project_types.ts index 86308e44..9dd1957d 100644 --- a/apps/rowboat/app/lib/types/project_types.ts +++ b/apps/rowboat/app/lib/types/project_types.ts @@ -14,6 +14,10 @@ export const ComposioConnectedAccount = z.object({ lastUpdatedAt: z.string().datetime(), }); +export const CustomMcpServer = z.object({ + serverUrl: z.string(), +}); + export const Project = z.object({ _id: z.string().uuid(), name: z.string(), @@ -29,6 +33,7 @@ export const Project = z.object({ testRunCounter: z.number().default(0), mcpServers: z.array(MCPServer).optional(), composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(), + customMcpServers: z.record(z.string(), CustomMcpServer).optional(), }); export const ProjectMember = z.object({ @@ -43,20 +48,4 @@ export const ApiKey = z.object({ key: z.string(), createdAt: z.string().datetime(), lastUsedAt: z.string().datetime().optional(), -}); - -export function mergeProjectTools( - workflowTools: z.infer[], - projectTools: z.infer[] -): z.infer[] { - // Filter out any existing MCP tools from workflow tools - const nonMcpTools = workflowTools.filter(t => !t.isMcp); - - // Merge with project tools - const merged = [ - ...nonMcpTools, - ...projectTools - ]; - - return merged; -} +}); \ No newline at end of file diff --git a/apps/rowboat/app/lib/types/types.ts b/apps/rowboat/app/lib/types/types.ts index 0900d748..dab9bdde 100644 --- a/apps/rowboat/app/lib/types/types.ts +++ b/apps/rowboat/app/lib/types/types.ts @@ -210,6 +210,5 @@ export function convertMcpServerToolToWorkflowTool( export const ZStreamAgentResponsePayload = z.object({ projectId: z.string(), workflow: Workflow, - projectTools: z.array(WorkflowTool), messages: z.array(Message), }); diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index 73e36935..2a8e5dbb 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -39,7 +39,6 @@ export const WorkflowTool = z.object({ name: z.string(), description: z.string(), mockTool: z.boolean().default(false).optional(), - autoSubmitMockedResponse: z.boolean().default(false).optional(), mockInstructions: z.string().optional(), parameters: z.object({ type: z.literal('object'), @@ -48,10 +47,9 @@ export const WorkflowTool = z.object({ additionalProperties: z.boolean().optional(), }), isMcp: z.boolean().default(false).optional(), - isLibrary: z.boolean().default(false).optional(), mcpServerName: z.string().optional(), - mcpServerURL: z.string().optional(), isComposio: z.boolean().optional(), // whether this is a Composio tool + isLibrary: z.boolean().default(false).optional(), // whether this is a library tool composioData: z.object({ slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE" noAuth: z.boolean(), // whether the tool requires no authentication @@ -67,14 +65,6 @@ export const Workflow = z.object({ startAgent: z.string(), lastUpdatedAt: z.string().datetime(), mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions - composioMockToolkitStates: z.record(z.string(), z.object({ - toolkitSlug: z.string(), - isMocked: z.boolean(), - mockInstructions: z.string().optional(), - autoSubmitMockedResponse: z.boolean().default(false), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), - })).optional(), }); export const WorkflowTemplate = Workflow .omit({ @@ -97,7 +87,6 @@ export function sanitizeTextWithMentions( tools: z.infer[], prompts: z.infer[], }, - projectTools: z.infer[] = [] ): { sanitized: string; entities: z.infer[]; @@ -127,8 +116,7 @@ export function sanitizeTextWithMentions( if (entity.type === 'agent') { return workflow.agents.some(a => a.name === entity.name); } else if (entity.type === 'tool') { - return workflow.tools.some(t => t.name === entity.name) || - projectTools.some(t => t.name === entity.name); + return workflow.tools.some(t => t.name === entity.name); } else if (entity.type === 'prompt') { return workflow.prompts.some(p => p.name === entity.name); } diff --git a/apps/rowboat/app/lib/utils.ts b/apps/rowboat/app/lib/utils.ts index c054d355..4d6ee2ff 100644 --- a/apps/rowboat/app/lib/utils.ts +++ b/apps/rowboat/app/lib/utils.ts @@ -8,7 +8,6 @@ import { Message, ZStreamAgentResponsePayload } from "./types/types"; export async function getAgenticResponseStreamId( projectId: string, workflow: z.infer, - projectTools: z.infer[], messages: z.infer[], ): Promise<{ streamId: string, @@ -16,7 +15,6 @@ export async function getAgenticResponseStreamId( const payload: z.infer = { projectId, workflow, - projectTools, messages, } // serialize the request diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index e977fc67..f74c63ed 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -42,7 +42,6 @@ export function AgentConfig({ usedAgentNames, agents, tools, - projectTools, prompts, dataSources, handleUpdate, @@ -57,7 +56,6 @@ export function AgentConfig({ usedAgentNames: Set, agents: z.infer[], tools: z.infer[], - projectTools: z.infer[], prompts: z.infer[], dataSources: WithStringId>[], handleUpdate: (agent: z.infer) => void, @@ -172,7 +170,7 @@ export function AgentConfig({ const atMentions = createAtMentions({ agents, prompts, - tools: [...tools, ...projectTools], + tools, currentAgentName: agent.name }); diff --git a/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx index f88c4dbc..fd758380 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx @@ -1,9 +1,10 @@ "use client"; import { WorkflowTool } from "../../../lib/types/workflow_types"; -import { Checkbox, Select, SelectItem, RadioGroup, Radio } from "@heroui/react"; +import { Checkbox, Select, SelectItem, Switch } from "@heroui/react"; import { z } from "zod"; -import { ImportIcon, XIcon, PlusIcon, FolderIcon} from "lucide-react"; +import { ImportIcon, XIcon, PlusIcon, FolderIcon, Globe, Zap, ExternalLink } from "lucide-react"; import { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; import { Textarea } from "@/components/ui/textarea"; import { Panel } from "@/components/common/panel-common"; import { Button } from "@/components/ui/button"; @@ -12,6 +13,7 @@ import { SectionCard } from "@/components/common/section-card"; import { ToolParamCard } from "@/components/common/tool-param-card"; import { UserIcon, Settings, Settings2 } from "lucide-react"; import { EditableField } from "@/app/lib/components/editable-field"; +import Link from "next/link"; // Update textarea styles with improved states const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500"; @@ -173,8 +175,11 @@ export function ToolConfig({ required: tool.parameters?.required || [] }); + const params = useParams(); + const projectId = params.projectId as string; const [selectedParams, setSelectedParams] = useState(new Set([])); - const isReadOnly = tool.isMcp || tool.isLibrary || tool.isComposio; + const isReadOnly = tool.isMcp || tool.isComposio; + const isWebhookTool = !tool.isMcp && !tool.isComposio; const [nameError, setNameError] = useState(null); // Log when parameters are being rendered @@ -337,6 +342,57 @@ export function ToolConfig({ } >
+ {/* Tool Type Section */} +
+
+
+ {tool.isMcp ? ( + + ) : tool.isComposio ? ( + + ) : ( + + )} +
+
+

+ How this tool runs +

+ {tool.isMcp ? ( +
+

This tool is powered by the {tool.mcpServerName} MCP server.

+

+ MCP (Model Context Protocol) tools are external services that provide additional capabilities to your agent. +

+
+ ) : tool.isComposio ? ( +
+
+

This tool is powered by Composio

+ {tool.composioData?.toolkitName && ( + + {tool.composioData.toolkitName} + + )} +
+

+ Composio provides pre-built integrations with popular services and APIs. +

+
+ ) : ( +
+
+

This tool is invoked using the webhook configured in project settings

+
+

+ Webhook tools make HTTP requests to your configured endpoint when called by the agent. +

+
+ )} +
+
+
+ {/* Identity Section */} } @@ -350,6 +406,7 @@ export function ToolConfig({
{ setNameError(validateToolName(value)); if (!validateToolName(value)) { @@ -375,6 +432,7 @@ export function ToolConfig({
handleUpdate({ ...tool, description: value })} multiline={true} @@ -385,56 +443,37 @@ export function ToolConfig({
- {/* Behavior Section */} + {/* Mock Section */} } - title="Behavior" - labelWidth="md:w-32" + title="Mock responses" + labelWidth="md:w-64" className="mb-1" >
- {!isReadOnly && ( -
- - +
+ handleUpdate({ ...tool, - mockTool: value === "mock", - autoSubmitMockedResponse: value === "mock" ? true : undefined + mockTool: value, })} - orientation="horizontal" - classNames={{ - wrapper: "flex flex-col md:flex-row gap-2 md:gap-8 pl-0 md:pl-3", - label: "text-sm" - }} - > - - Mock tool responses - - - Connect tool to your API - - + size="sm" + color="primary" + /> +
- )} + + When enabled, this tool will be mocked. + +
{tool.mockTool && ( -
- - Describe the response the mock tool should return. This will be shown in the chat when the tool is called. You can also provide a JSON schema for the response. +
+ + Describe the response the mock tool should return. This will be shown in the chat when the tool is called. handleUpdate({ @@ -445,20 +484,6 @@ export function ToolConfig({ placeholder="Mock response instructions..." className="w-full text-xs p-2 bg-white dark:bg-gray-900" /> - handleUpdate({ - ...tool, - autoSubmitMockedResponse: value - })} - disabled={isReadOnly} - className="mt-2" - > - - Automatically send mock response in chat - -
)}
diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 5ab76f37..30519109 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -19,10 +19,8 @@ export function App({ workflow, messageSubscriber, mcpServerUrls, - toolWebhookUrl, isInitialState = false, onPanelClick, - projectTools, triggerCopilotChat, }: { hidden?: boolean; @@ -30,10 +28,8 @@ export function App({ workflow: z.infer; messageSubscriber?: (messages: z.infer[]) => void; mcpServerUrls: Array>; - toolWebhookUrl: string; isInitialState?: boolean; onPanelClick?: () => void; - projectTools: z.infer[]; triggerCopilotChat?: (message: string) => void; }) { const [counter, setCounter] = useState(0); @@ -156,10 +152,8 @@ export function App({ systemMessage={systemMessage} onSystemMessageChange={handleSystemMessageChange} mcpServerUrls={mcpServerUrls} - toolWebhookUrl={toolWebhookUrl} onCopyClick={(fn) => { getCopyContentRef.current = fn; }} showDebugMessages={showDebugMessages} - projectTools={projectTools} triggerCopilotChat={triggerCopilotChat} />
diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx index a6b2ee86..473ef311 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx @@ -22,11 +22,9 @@ export function Chat({ systemMessage, onSystemMessageChange, mcpServerUrls, - toolWebhookUrl, onCopyClick, showDebugMessages = true, showJsonMode = false, - projectTools, triggerCopilotChat, }: { chat: z.infer; @@ -36,11 +34,9 @@ export function Chat({ systemMessage: string; onSystemMessageChange: (message: string) => void; mcpServerUrls: Array>; - toolWebhookUrl: string; onCopyClick: (fn: () => string) => void; showDebugMessages?: boolean; showJsonMode?: boolean; - projectTools: z.infer[]; triggerCopilotChat?: (message: string) => void; }) { const [messages, setMessages] = useState[]>(chat.messages); @@ -210,7 +206,6 @@ export function Chat({ const response = await getAssistantResponseStreamId( projectId, workflow, - projectTools, [ { role: 'system', @@ -336,9 +331,7 @@ export function Chat({ workflow, systemMessage, mcpServerUrls, - toolWebhookUrl, fetchResponseError, - projectTools, ]); // Add a stop handler function diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/AddWebhookTool.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/AddWebhookTool.tsx new file mode 100644 index 00000000..6935c4ed --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/tools/components/AddWebhookTool.tsx @@ -0,0 +1,42 @@ +'use client'; + +import React from 'react'; +import { WebhookConfig } from './WebhookConfig'; +import { Button } from '@heroui/react'; +import { WorkflowTool } from '@/app/lib/types/workflow_types'; +import { z } from 'zod'; + +interface AddWebhookToolProps { + projectId: string; + onAddTool: (tool: Partial>) => void; +} + +export function AddWebhookTool({ projectId, onAddTool }: AddWebhookToolProps) { + function handleAddTool() { + onAddTool({ + description: 'Webhook tool', + mockTool: false, + }); + } + + return ( +
+
+

+ Add webhook tool +

+
+ + + +
+ Click here to add a webhook tool: +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx index 380bb5b7..c66a4de6 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx @@ -3,25 +3,30 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; -import { Info, RefreshCw, Search } from 'lucide-react'; +import { RefreshCw, Search } from 'lucide-react'; import clsx from 'clsx'; -import { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions'; +import { listToolkits } from '@/app/actions/composio_actions'; import { getProjectConfig } from '@/app/actions/project_actions'; import { z } from 'zod'; import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio'; import { Project } from '@/app/lib/types/project_types'; import { ComposioToolsPanel } from './ComposioToolsPanel'; import { ToolkitCard } from './ToolkitCard'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; type ToolkitType = z.infer; type ToolkitListResponse = z.infer>>; type ProjectType = z.infer; -export function Composio() { - const params = useParams(); - const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; - if (!projectId) throw new Error('Project ID is required'); - +export function Composio({ + projectId, + tools, + onAddTool +}: { + projectId: string; + tools: z.infer; + onAddTool: (tool: z.infer) => void; +}) { const [toolkits, setToolkits] = useState([]); const [projectConfig, setProjectConfig] = useState(null); const [loading, setLoading] = useState(true); @@ -29,8 +34,6 @@ export function Composio() { const [searchQuery, setSearchQuery] = useState(''); const [selectedToolkit, setSelectedToolkit] = useState(null); const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false); - const [savingTools, setSavingTools] = useState(false); - const [composioSelectedTools, setComposioSelectedTools] = useState[]>([]); const loadProjectConfig = useCallback(async () => { try { @@ -42,15 +45,6 @@ export function Composio() { } }, [projectId]); - const loadComposioSelectedTools = useCallback(async () => { - try { - const tools = await getComposioToolsFromWorkflow(projectId); - setComposioSelectedTools(tools); - } catch (err: any) { - console.error('Error fetching composio selected tools:', err); - } - }, [projectId]); - const loadAllToolkits = useCallback(async () => { let cursor: string | null = null; let allToolkits: ToolkitType[] = []; @@ -85,7 +79,7 @@ export function Composio() { } }, [projectId]); - const handleManageTools = useCallback((toolkit: ToolkitType) => { + const handleSelectToolkit = useCallback((toolkit: ToolkitType) => { setSelectedToolkit(toolkit); setIsToolsPanelOpen(true); }, []); @@ -95,69 +89,9 @@ export function Composio() { setIsToolsPanelOpen(false); }, []); - const handleProjectConfigUpdate = useCallback(() => { - loadProjectConfig(); - loadComposioSelectedTools(); - }, [loadProjectConfig, loadComposioSelectedTools]); - - const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer[]) => { - if (!projectId) return; - - setSavingTools(true); - try { - // Get existing selected tools from workflow - const existingSelectedTools = composioSelectedTools; - - // Create a map of existing tools by slug for easy lookup - const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool])); - - // Add or update the new selections - for (const tool of selectedToolObjects) { - existingToolsMap.set(tool.slug, tool); - } - - // Convert back to array - const mergedSelectedTools = Array.from(existingToolsMap.values()); - - await updateComposioSelectedTools(projectId, mergedSelectedTools); - - // Refresh data to get updated tools - await loadComposioSelectedTools(); - } catch (error) { - console.error('Error saving tool selection:', error); - } finally { - setSavingTools(false); - } - }, [projectId, composioSelectedTools, loadComposioSelectedTools]); - - const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => { - if (!projectId) return; - - setSavingTools(true); - try { - // Get existing selected tools from workflow - const existingSelectedTools = composioSelectedTools; - - // Filter out all tools from the specified toolkit - const filteredSelectedTools = existingSelectedTools.filter(tool => - tool.toolkit.slug !== toolkitSlug - ); - - await updateComposioSelectedTools(projectId, filteredSelectedTools); - - // Refresh data to get updated tools - await loadComposioSelectedTools(); - } catch (error) { - console.error('Error removing toolkit tools:', error); - } finally { - setSavingTools(false); - } - }, [projectId, composioSelectedTools, loadComposioSelectedTools]); - useEffect(() => { loadProjectConfig(); - loadComposioSelectedTools(); - }, [loadProjectConfig, loadComposioSelectedTools]); + }, [loadProjectConfig]); useEffect(() => { loadAllToolkits(); @@ -257,19 +191,14 @@ export function Composio() {
{filteredToolkits.map((toolkit) => { const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE'; - const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id; return ( handleManageTools(toolkit)} - onProjectConfigUpdate={handleProjectConfigUpdate} - onRemoveToolkitTools={handleRemoveToolkitTools} + workflowTools={tools} + onSelectToolkit={() => handleSelectToolkit(toolkit)} /> ); })} @@ -284,16 +213,13 @@ export function Composio() { )} {/* Tools Panel */} - + tools={tools} + onAddTool={onAddTool} + />}
); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx index e05140a7..cd0fa538 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx @@ -1,20 +1,18 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { useParams } from 'next/navigation'; import { PictureImg } from '@/components/ui/picture-img'; -import { Button, Checkbox } from '@heroui/react'; -import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react'; -import { listTools, deleteConnectedAccount, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions'; +import { Button, Checkbox, Input } from '@heroui/react'; +import { ChevronLeft, ChevronRight, Search, X } from 'lucide-react'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; +import { listTools } from '@/app/actions/composio_actions'; import { z } from 'zod'; import { ZTool, ZListResponse } from '@/app/lib/composio/composio'; import { SlidePanel } from '@/components/ui/slide-panel'; -import { Project } from '@/app/lib/types/project_types'; -import { ToolkitAuthModal } from './ToolkitAuthModal'; type ToolType = z.infer; type ToolListResponse = z.infer>>; -type ProjectType = z.infer; interface ComposioToolsPanelProps { toolkit: { @@ -24,25 +22,19 @@ interface ComposioToolsPanelProps { logo: string; }; no_auth?: boolean; - } | null; + }; isOpen: boolean; onClose: () => void; - projectConfig: ProjectType | null; - onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void; - onProjectConfigUpdate: () => void; - onRemoveToolkitTools: (toolkitSlug: string) => void; - isSaving: boolean; + tools: z.infer; + onAddTool: (tool: z.infer) => void; } export function ComposioToolsPanel({ toolkit, isOpen, onClose, - projectConfig, - onUpdateToolsSelection, - onProjectConfigUpdate, - onRemoveToolkitTools, - isSaving + tools: workflowTools, + onAddTool, }: ComposioToolsPanelProps) { const params = useParams(); const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; @@ -55,15 +47,30 @@ export function ComposioToolsPanel({ const [cursorHistory, setCursorHistory] = useState([]); const [selectedTools, setSelectedTools] = useState>(new Set()); const [hasChanges, setHasChanges] = useState(false); - const [showAuthModal, setShowAuthModal] = useState(false); - const [isProcessingAuth, setIsProcessingAuth] = useState(false); - const [composioSelectedTools, setComposioSelectedTools] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); - const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => { + const selectedToolSlugs = workflowTools + .filter(tool => tool.isComposio && tool.composioData?.toolkitSlug === toolkit.slug) + .map(tool => tool.composioData!.slug); + + // Filter out already selected tools + const availableTools = tools.filter(tool => !selectedToolSlugs.includes(tool.slug)); + + // Debounce search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 300); + + return () => clearTimeout(timer); + }, [searchQuery]); + + const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null, search: string | null = null) => { try { setToolsLoading(true); - const response: ToolListResponse = await listTools(projectId, toolkitSlug, cursor); + const response: ToolListResponse = await listTools(projectId, toolkitSlug, search, cursor); setTools(response.items); setNextCursor(response.next_cursor); @@ -81,27 +88,25 @@ export function ComposioToolsPanel({ } }, [projectId]); - const loadComposioSelectedTools = useCallback(async () => { - try { - const tools = await getComposioToolsFromWorkflow(projectId); - setComposioSelectedTools(tools); - } catch (err: any) { - console.error('Error fetching composio selected tools:', err); + // Load tools when search query changes + useEffect(() => { + if (toolkit && isOpen) { + loadToolsForToolkit(toolkit.slug, null, debouncedSearchQuery || null); } - }, [projectId]); + }, [toolkit, isOpen, debouncedSearchQuery, loadToolsForToolkit]); const handleNextPage = useCallback(async () => { - if (!nextCursor || !toolkit) return; + if (!nextCursor) return; // Add current cursor to history setCursorHistory(prev => [...prev, currentCursor || '']); setCurrentCursor(nextCursor); - await loadToolsForToolkit(toolkit.slug, nextCursor); - }, [nextCursor, toolkit, currentCursor, loadToolsForToolkit]); + await loadToolsForToolkit(toolkit.slug, nextCursor, debouncedSearchQuery || null); + }, [nextCursor, toolkit, currentCursor, debouncedSearchQuery, loadToolsForToolkit]); const handlePreviousPage = useCallback(async () => { - if (cursorHistory.length === 0 || !toolkit) return; + if (cursorHistory.length === 0) return; // Get the previous cursor from history const previousCursor = cursorHistory[cursorHistory.length - 1]; @@ -110,8 +115,8 @@ export function ComposioToolsPanel({ setCursorHistory(newHistory); setCurrentCursor(previousCursor); - await loadToolsForToolkit(toolkit.slug, previousCursor); - }, [cursorHistory, toolkit, loadToolsForToolkit]); + await loadToolsForToolkit(toolkit.slug, previousCursor, debouncedSearchQuery || null); + }, [cursorHistory, toolkit, debouncedSearchQuery, loadToolsForToolkit]); const handleToolSelectionChange = useCallback((toolSlug: string, selected: boolean) => { setSelectedTools(prev => { @@ -126,243 +131,195 @@ export function ComposioToolsPanel({ }); }, []); - const handleSaveTools = useCallback(async () => { - // Convert selected tool slugs to actual tool objects + const handleAddSelectedTools = useCallback(() => { + // Convert selected tool slugs to actual tool objects and add them const selectedToolObjects = tools.filter(tool => selectedTools.has(tool.slug)); - await onUpdateToolsSelection(selectedToolObjects); - setHasChanges(false); - }, [onUpdateToolsSelection, selectedTools, tools]); - - const handleConnect = useCallback(() => { - setShowAuthModal(true); - }, []); - - const handleDisconnect = useCallback(async () => { - if (!toolkit) return; - const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id; + selectedToolObjects.forEach(tool => { + const toolToAdd = { + name: tool.name, + description: tool.description, + parameters: { + type: 'object' as const, + properties: tool.input_parameters?.properties || {}, + required: tool.input_parameters?.required || [], + }, + isComposio: true, + composioData: { + slug: tool.slug, + noAuth: toolkit.no_auth || false, + toolkitName: toolkit.name, + toolkitSlug: toolkit.slug, + logo: toolkit.meta.logo, + }, + }; + + onAddTool(toolToAdd); + }); - setIsProcessingAuth(true); - try { - if (connectedAccountId) { - await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId); - onProjectConfigUpdate(); - onRemoveToolkitTools(toolkit.slug); - } - } catch (err: any) { - console.error('Disconnect failed:', err); - } finally { - setIsProcessingAuth(false); - } - }, [projectId, toolkit, projectConfig, onProjectConfigUpdate, onRemoveToolkitTools]); - - const handleAuthComplete = useCallback(() => { - setShowAuthModal(false); - onProjectConfigUpdate(); - }, [onProjectConfigUpdate]); + onClose(); + }, [selectedTools, tools, toolkit, onAddTool, onClose]); const handleClose = useCallback(() => { setTools([]); setSelectedTools(new Set()); setHasChanges(false); - if (hasChanges) { - if (window.confirm('You have unsaved changes. Are you sure you want to close?')) { - onClose(); - } - } else { - onClose(); - } - }, [onClose, hasChanges]); + setSearchQuery(''); + setDebouncedSearchQuery(''); + onClose(); + }, [onClose]); - // Initialize selected tools from workflow when opening the panel - useEffect(() => { - if (toolkit && isOpen) { - loadComposioSelectedTools(); - } - }, [toolkit, isOpen, loadComposioSelectedTools]); - - // Set selected tools when composioSelectedTools is loaded - useEffect(() => { - if (toolkit && composioSelectedTools.length > 0) { - const toolSlugs = new Set(composioSelectedTools.map(tool => tool.slug)); - setSelectedTools(toolSlugs); - setHasChanges(false); - } - }, [toolkit, composioSelectedTools]); - - useEffect(() => { - if (toolkit && isOpen) { - loadToolsForToolkit(toolkit.slug, null); - } - }, [toolkit, isOpen, loadToolsForToolkit]); + const handleClearSearch = useCallback(() => { + setSearchQuery(''); + }, []); if (!toolkit) return null; - // Check if the toolkit is connected (has an active connected account) or doesn't require auth - const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE'; - return ( - <> - - {toolkit.meta.logo && ( - - )} - {toolkit.name} -
- } - > -
- {/* Connection Status Banner */} - {!toolkit.no_auth && ( -
-
-
-
-
-

- {isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'} -

-

- {isToolkitConnected - ? 'You can select and use tools from this toolkit' - : 'You can select tools now. Authentication will be required in the build view to use them.' - } -

-
-
- {isToolkitConnected && ( - - )} -
-
+ + {toolkit.meta.logo && ( + )} - - {/* Header */} -
-
-

Available Tools

-
- {hasChanges && ( - - )} -
+ {toolkit.name} +
+ } + > +
+ {/* Header */} +
+
+
+

Select Tools

+

+ Check the tools you want to add to your workflow +

+ {hasChanges && ( + + )}
- {/* Scrollable Tools List */} -
- {toolsLoading ? ( -
-
-

Loading tools...

-
- ) : ( -
- {tools.map((tool) => ( -
-
- handleToolSelectionChange(tool.slug, selected)} - size="sm" - /> -
-

- {tool.name} -

-

- {tool.description} -

+ {/* Search Box */} +
+
+ +
+ setSearchQuery(e.target.value)} + className="pl-10 pr-10" + size="sm" + /> + {searchQuery && ( + + )} +
+
+ + {/* Scrollable Tools List */} +
+ {toolsLoading ? ( +
+
+

+ {searchQuery ? 'Searching tools...' : 'Loading tools...'} +

+
+ ) : tools.length === 0 ? ( +
+

+ {searchQuery ? 'No tools found matching your search.' : 'No tools available.'} +

+
+ ) : ( +
+ {availableTools.map((tool) => ( +
+
+ handleToolSelectionChange(tool.slug, selected)} + size="sm" + /> +
+

+ {tool.name} +

+
+ {tool.slug}
+

+ {tool.description} +

- ))} -
- )} -
+
+ ))} +
+ )} +
- {/* Fixed Pagination Controls */} -
-
-
- - -
+ {/* Fixed Pagination Controls */} +
+
+
+ {availableTools.length > 0 && ( + + {availableTools.length} tool{availableTools.length !== 1 ? 's' : ''} found + {searchQuery && ` for "${searchQuery}"`} + + )} +
+
+ +
- - - {/* Auth Modal */} - {toolkit && ( - setShowAuthModal(false)} - toolkitSlug={toolkit.slug} - projectId={projectId} - onComplete={handleAuthComplete} - /> - )} - +
+ ); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx new file mode 100644 index 00000000..5ee7becc --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useParams } from 'next/navigation'; +import { Button } from '@heroui/react'; +import { Input } from '@/components/ui/input'; +import { Info, Plus, Trash2 } from 'lucide-react'; +import { z } from 'zod'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; +import { getProjectConfig } from '@/app/actions/project_actions'; +import { addServer, removeServer } from '@/app/actions/custom_mcp_server_actions'; +import { fetchTools } from "@/app/actions/custom_mcp_server_actions"; +import { ServerCard } from './ServerCard'; +import { McpToolsPanel } from './McpToolsPanel'; +import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal'; + +// Types +const CustomMcpServerType = z.object({ serverUrl: z.string() }); +type CustomMcpServer = z.infer; + +type ServerList = Record; + +type CustomMcpServersProps = { + tools: z.infer; + onAddTool: (tool: z.infer) => void; +}; + +export function CustomMcpServers({ tools: workflowTools, onAddTool }: CustomMcpServersProps) { + const params = useParams(); + const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; + if (!projectId) throw new Error('Project ID is required'); + + // State + const [servers, setServers] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [addName, setAddName] = useState(''); + const [addUrl, setAddUrl] = useState(''); + const [addLoading, setAddLoading] = useState(false); + const [addError, setAddError] = useState(null); + const [panelServer, setPanelServer] = useState<{ name: string; url: string } | null>(null); + const [toolsLoading, setToolsLoading] = useState(false); + const [toolsError, setToolsError] = useState(null); + const [serverTools, setServerTools] = useState[]>([]); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [serverToDelete, setServerToDelete] = useState(null); + + // Fetch servers on mount + const fetchServers = useCallback(async () => { + setLoading(true); + setError(null); + try { + const project = await getProjectConfig(projectId); + setServers(project.customMcpServers || {}); + } catch (err: any) { + setError(err?.message || 'Failed to load servers'); + setServers({}); + } finally { + setLoading(false); + } + }, [projectId]); + + useEffect(() => { + fetchServers(); + }, [fetchServers]); + + // Add server + const handleAddServer = async (e: React.FormEvent) => { + e.preventDefault(); + if (!addName || !addUrl) return; + setAddLoading(true); + setAddError(null); + try { + await addServer(projectId, addName, { serverUrl: addUrl }); + setAddName(''); + setAddUrl(''); + await fetchServers(); + } catch (err: any) { + setAddError(err?.message || 'Failed to add server'); + } finally { + setAddLoading(false); + } + }; + + // Open delete modal + const handleDeleteClick = (name: string) => { + setServerToDelete(name); + setDeleteModalOpen(true); + }; + + // Delete server + const handleDeleteServer = async () => { + if (!serverToDelete) return; + try { + await removeServer(projectId, serverToDelete); + await fetchServers(); + setDeleteModalOpen(false); + setServerToDelete(null); + } catch (err: any) { + alert(err?.message || 'Failed to delete server'); + } + }; + + // Open panel and fetch tools + const handleOpenPanel = async (name: string, url: string) => { + setPanelServer({ name, url }); + setToolsLoading(true); + setToolsError(null); + setServerTools([]); + try { + const fetched = await fetchTools(url, name); + setServerTools(fetched); + } catch (err: any) { + setToolsError(err?.message || 'Failed to fetch tools'); + } finally { + setToolsLoading(false); + } + }; + + // Close panel + const handleClosePanel = () => { + setPanelServer(null); + setServerTools([]); + }; + + // UI + return ( +
+
+
+
+ +
+

+ Add your own MCP servers here. Enter the server details and select tools to add to your workflow. +

+
+
+ + {/* Add server form */} +
+
+ setAddName(e.target.value)} + placeholder="Server Name" + required + className="flex-1" + /> + setAddUrl(e.target.value)} + placeholder="Server URL" + required + className="flex-1" + /> + +
+ {addError &&
{addError}
} +
+ + {/* Server cards */} + {loading ? ( +
+
+

Loading servers...

+
+ ) : error ? ( +
{error}
+ ) : ( +
+ {Object.entries(servers).length === 0 ? ( +
No custom MCP servers added yet.
+ ) : ( + Object.entries(servers).map(([name, { serverUrl }]) => ( + handleOpenPanel(name, serverUrl)} + onDeleteServer={() => handleDeleteClick(name)} + /> + )) + )} +
+ )} + + {/* Delete confirmation modal */} + setDeleteModalOpen(false)} + onConfirm={handleDeleteServer} + title="Delete Server" + confirmationQuestion={`Are you sure you want to delete "${serverToDelete}"? This will delete the server from the project.`} + confirmButtonText="Delete" + /> + + {/* MCP Tools Panel */} + +
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx deleted file mode 100644 index ccf95d69..00000000 --- a/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx +++ /dev/null @@ -1,475 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback } from 'react'; -import { useParams } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { Info, Plus, Search } from 'lucide-react'; -import { clsx } from 'clsx'; -import { z } from 'zod'; -import { MCPServer } from '@/app/lib/types/types'; -import { - ServerCard, - ToolManagementPanel -} from './MCPServersCommon'; -import { fetchMcpToolsForServer } from '@/app/actions/mcp_actions'; -import { - fetchCustomServers, - addCustomServer, - removeCustomServer, - toggleCustomServer, - updateCustomServerTools -} from '@/app/actions/custom_server_actions'; -import { Modal } from '@/components/ui/modal'; - -type McpServerType = z.infer; -type McpToolType = z.infer['tools'][number]; - -export function CustomServers({ onToolsUpdated }: { onToolsUpdated?: () => void }) { - const params = useParams(); - const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; - if (!projectId) throw new Error('Project ID is required'); - - const [servers, setServers] = useState([]); - const [selectedServer, setSelectedServer] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [togglingServers, setTogglingServers] = useState>(new Set()); - const [serverOperations, setServerOperations] = useState>(new Map()); - const [selectedTools, setSelectedTools] = useState>(new Set()); - const [hasToolChanges, setHasToolChanges] = useState(false); - const [savingTools, setSavingTools] = useState(false); - const [syncingServers, setSyncingServers] = useState>(new Set()); - const [showAddServer, setShowAddServer] = useState(false); - const [newServerName, setNewServerName] = useState(''); - const [newServerUrl, setNewServerUrl] = useState(''); - - const fetchServers = useCallback(async () => { - try { - setLoading(true); - const customServers = await fetchCustomServers(projectId); - setServers(customServers); - setError(null); - } catch (err: any) { - setError(err?.message || 'Failed to load custom MCP servers'); - console.error('Error fetching servers:', err); - setServers([]); - } finally { - setLoading(false); - } - }, [projectId]); - - useEffect(() => { - fetchServers(); - }, [fetchServers]); - - const handleToggleServer = async (server: McpServerType) => { - try { - const serverKey = server.name; - setTogglingServers(prev => { - const next = new Set(prev); - next.add(serverKey); - return next; - }); - - setServerOperations(prev => { - const next = new Map(prev); - next.set(serverKey, server.isActive ? 'delete' : 'setup'); - return next; - }); - - await toggleCustomServer(projectId, server.name, !server.isActive); - - // Update local state - setServers(prevServers => { - return prevServers.map(s => { - if (s.name === serverKey) { - return { - ...s, - isActive: !s.isActive - }; - } - return s; - }); - }); - - // Notify parent component about tool updates - onToolsUpdated?.(); - } catch (err) { - console.error('Toggle failed:', { server: server.name, error: err }); - } finally { - const serverKey = server.name; - setTogglingServers(prev => { - const next = new Set(prev); - next.delete(serverKey); - return next; - }); - setServerOperations(prev => { - const next = new Map(prev); - next.delete(serverKey); - return next; - }); - } - }; - - const handleSyncServer = async (server: McpServerType) => { - if (!projectId || !server.isActive) return; - - try { - setSyncingServers(prev => { - const next = new Set(prev); - next.add(server.name); - return next; - }); - const enrichedTools = await fetchMcpToolsForServer(projectId, server.name); - - const updatedAvailableTools = enrichedTools.map(tool => ({ - id: tool.name, - name: tool.name, - description: tool.description, - parameters: tool.parameters - })); - - await updateCustomServerTools( - projectId, - server.name, - updatedAvailableTools, // Auto-select all tools for custom servers - updatedAvailableTools - ); - - // Update servers state - setServers(prevServers => { - return prevServers.map(s => { - if (s.name === server.name) { - return { - ...s, - availableTools: updatedAvailableTools, - tools: updatedAvailableTools - }; - } - return s; - }); - }); - - // If this server is currently selected, update the selectedTools state - if (selectedServer?.name === server.name) { - setSelectedServer(prev => { - if (!prev) return null; - return { - ...prev, - availableTools: updatedAvailableTools, - tools: updatedAvailableTools - }; - }); - // Update selectedTools to include all tools for the custom server - setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id))); - } - - // Notify parent component about tool updates - onToolsUpdated?.(); - } finally { - setSyncingServers(prev => { - const next = new Set(prev); - next.delete(server.name); - return next; - }); - } - }; - - // Add effect to sync selectedTools when selectedServer changes - useEffect(() => { - if (selectedServer) { - setSelectedTools(new Set(selectedServer.tools.map(tool => tool.id))); - setHasToolChanges(false); - } - }, [selectedServer]); - - const handleAddServer = async () => { - if (!newServerName || !newServerUrl) return; - - try { - const newServer: McpServerType = { - id: `custom-${Date.now()}`, - name: newServerName, - description: `Custom MCP server at ${newServerUrl}`, - serverUrl: newServerUrl, - tools: [], - availableTools: [], - isActive: true, - isReady: true, - serverType: 'custom', - authNeeded: false, - isAuthenticated: false - }; - - // Add to MongoDB and get back the formatted server - const formattedServer = await addCustomServer(projectId, newServer); - - // Update local state with the formatted server - setServers(prev => [...prev, formattedServer]); - setShowAddServer(false); - setNewServerName(''); - setNewServerUrl(''); - - // Fetch tools for the new server using the formatted URL - await handleSyncServer(formattedServer); - - // Notify parent component about tool updates - onToolsUpdated?.(); - } catch (err) { - console.error('Error adding server:', err); - setError('Failed to add server. Please try again.'); - } - }; - - const handleRemoveServer = async (server: McpServerType) => { - // Show confirmation dialog - const shouldRemove = window.confirm( - "Are you sure you want to delete this server? Alternatively, you can toggle it OFF if you'd like to retain the configuration but not make it available to agents." - ); - - if (!shouldRemove) return; - - try { - await removeCustomServer(projectId, server.name); - // Update local state - setServers(prev => prev.filter(s => s.name !== server.name)); - // If this server was selected, close the tool management panel - if (selectedServer?.name === server.name) { - setSelectedServer(null); - } - - // Notify parent component about tool updates - onToolsUpdated?.(); - } catch (err) { - console.error('Error removing server:', err); - setError('Failed to remove server. Please try again.'); - } - }; - - const handleSaveToolSelection = async () => { - if (!selectedServer || !projectId) return; - - setSavingTools(true); - try { - const availableTools = selectedServer.availableTools || []; - const selectedToolsList = availableTools.filter(tool => selectedTools.has(tool.id)); - - await updateCustomServerTools( - projectId, - selectedServer.name, - selectedToolsList, - availableTools - ); - - setServers(prevServers => { - return prevServers.map(s => { - if (s.name === selectedServer.name) { - return { - ...s, - tools: selectedToolsList - }; - } - return s; - }); - }); - - setSelectedServer(prev => { - if (!prev) return null; - return { - ...prev, - tools: selectedToolsList - }; - }); - - setHasToolChanges(false); - - // Notify parent component about tool updates - onToolsUpdated?.(); - } catch (error) { - console.error('Error saving tool selection:', error); - } finally { - setSavingTools(false); - } - }; - - const filteredServers = servers.filter(server => { - const searchLower = searchQuery.toLowerCase(); - const serverTools = server.tools || []; - return ( - server.name.toLowerCase().includes(searchLower) || - server.description.toLowerCase().includes(searchLower) || - serverTools.some(tool => - tool.name.toLowerCase().includes(searchLower) || - tool.description.toLowerCase().includes(searchLower) - ) - ); - }); - - return ( -
-
-
-
- -
-

- Add your own MCP servers here. These servers will be available to agents in the Build view once toggled ON. -

-
-
- -
- -
-
-
- -
- setSearchQuery(e.target.value)} - className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md - bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 - placeholder-gray-400 dark:placeholder-gray-500 - focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 - hover:border-gray-300 dark:hover:border-gray-600 transition-colors" - /> -
-
- {filteredServers.length} {filteredServers.length === 1 ? 'server' : 'servers'} • { - filteredServers.reduce((total, server) => total + (server.availableTools?.length || 0), 0) - } tools -
-
-
- - { - setShowAddServer(false); - setNewServerName(''); - setNewServerUrl(''); - }} - title="Add Custom MCP Server" - > -
-
- - setNewServerName(e.target.value)} - placeholder="e.g., My Custom Server" - className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md - bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 - focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400" - /> -
-
- - setNewServerUrl(e.target.value)} - placeholder="e.g., http://localhost:3000" - className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md - bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 - focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400" - /> -
-
- - -
-
-
- - {loading ? ( -
-
-

Loading servers...

-
- ) : error ? ( -
{error}
- ) : ( -
- {filteredServers.map((server) => ( - handleToggleServer(server)} - onManageTools={() => setSelectedServer(server)} - onSync={() => handleSyncServer(server)} - onRemove={() => handleRemoveServer(server)} - isToggling={togglingServers.has(server.name)} - isSyncing={syncingServers.has(server.name)} - operation={serverOperations.get(server.name)} - error={error && error.includes(server.name) ? { message: error } : undefined} - showAuth={false} - /> - ))} -
- )} - - { - setSelectedServer(null); - setSelectedTools(new Set()); - setHasToolChanges(false); - }} - selectedTools={selectedTools} - onToolSelectionChange={(toolId, selected) => { - setSelectedTools(prev => { - const next = new Set(prev); - if (selected) { - next.add(toolId); - } else { - next.delete(toolId); - } - setHasToolChanges(true); - return next; - }); - }} - onSaveTools={handleSaveToolSelection} - onSyncTools={selectedServer ? () => handleSyncServer(selectedServer) : undefined} - hasChanges={hasToolChanges} - isSaving={savingTools} - isSyncing={selectedServer ? syncingServers.has(selectedServer.name) : false} - /> -
- ); -} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/McpToolsPanel.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/McpToolsPanel.tsx new file mode 100644 index 00000000..a3f5e9d5 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/tools/components/McpToolsPanel.tsx @@ -0,0 +1,240 @@ +'use client'; + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { Button, Checkbox, Input } from '@heroui/react'; +import { Search, X } from 'lucide-react'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; +import { z } from 'zod'; +import { SlidePanel } from '@/components/ui/slide-panel'; + +interface McpToolsPanelProps { + server: { + name: string; + url: string; + } | null; + isOpen: boolean; + onClose: () => void; + tools: z.infer; + onAddTool: (tool: z.infer) => void; + serverTools: z.infer[]; + toolsLoading: boolean; + toolsError: string | null; +} + +export function McpToolsPanel({ + server, + isOpen, + onClose, + tools: workflowTools, + onAddTool, + serverTools, + toolsLoading, + toolsError, +}: McpToolsPanelProps) { + const [selectedTools, setSelectedTools] = useState>(new Set()); + const [hasChanges, setHasChanges] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); + + // Filter out already selected tools + const selectedToolNames = workflowTools + .filter(tool => tool.isMcp && tool.mcpServerName === server?.name) + .map(tool => tool.name); + + // Debounce search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 300); + + return () => clearTimeout(timer); + }, [searchQuery]); + + // Filter tools based on search query + const filteredTools = useMemo(() => { + if (!debouncedSearchQuery) return serverTools; + + const query = debouncedSearchQuery.toLowerCase(); + return serverTools.filter(tool => + tool.name.toLowerCase().includes(query) || + tool.description.toLowerCase().includes(query) + ); + }, [serverTools, debouncedSearchQuery]); + + // Filter out already added tools + const availableTools = filteredTools.filter(tool => !selectedToolNames.includes(tool.name)); + + const handleToolSelectionChange = useCallback((toolName: string, selected: boolean) => { + setSelectedTools(prev => { + const next = new Set(prev); + if (selected) { + next.add(toolName); + } else { + next.delete(toolName); + } + setHasChanges(true); + return next; + }); + }, []); + + const handleAddSelectedTools = useCallback(() => { + // Convert selected tool names to actual tool objects and add them + const selectedToolObjects = serverTools.filter(tool => selectedTools.has(tool.name)); + + selectedToolObjects.forEach(tool => { + onAddTool(tool); + }); + + onClose(); + }, [selectedTools, serverTools, onAddTool, onClose]); + + const handleClose = useCallback(() => { + setSelectedTools(new Set()); + setHasChanges(false); + setSearchQuery(''); + setDebouncedSearchQuery(''); + onClose(); + }, [onClose]); + + const handleClearSearch = useCallback(() => { + setSearchQuery(''); + }, []); + + if (!server) return null; + + return ( + +
+ MCP +
+ {server.name} +
+ } + > +
+ {/* Header */} +
+
+
+

Select Tools

+

+ Check the tools you want to add to your workflow +

+
+ {hasChanges && ( + + )} +
+ + {/* Search Box */} +
+
+ +
+ setSearchQuery(e.target.value)} + className="pl-10 pr-10" + size="sm" + /> + {searchQuery && ( + + )} +
+
+ + {/* Error Display */} + {toolsError && ( +
+

{toolsError}

+
+ )} + + {/* Scrollable Tools List */} +
+ {toolsLoading ? ( +
+
+

+ {searchQuery ? 'Searching tools...' : 'Loading tools...'} +

+
+ ) : availableTools.length === 0 ? ( +
+

+ {searchQuery ? 'No tools found matching your search.' : 'No tools available.'} +

+
+ ) : ( +
+ {availableTools.map((tool) => ( +
+
+ handleToolSelectionChange(tool.name, selected)} + size="sm" + /> +
+

+ {tool.name} +

+

+ {tool.description} +

+
+
+
+ ))} +
+ )} +
+ + {/* Fixed Footer */} +
+
+
+ {availableTools.length > 0 && ( + + {availableTools.length} tool{availableTools.length !== 1 ? 's' : ''} found + {searchQuery && ` for "${searchQuery}"`} + + )} +
+
+ +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ServerCard.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ServerCard.tsx new file mode 100644 index 00000000..88d32667 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ServerCard.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { PictureImg } from '@/components/ui/picture-img'; +import clsx from 'clsx'; +import { z } from 'zod'; +import { Chip } from '@heroui/react'; +import { Server, MoreVertical } from 'lucide-react'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; +import { fetchTools } from "@/app/actions/custom_mcp_server_actions"; +import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react'; +import { Button } from '@heroui/react'; + +type ServerCardProps = { + serverName: string; + serverUrl: string; + workflowTools: z.infer; + onSelectServer: () => void; + onDeleteServer: () => void; +}; + +const serverCardStyles = { + base: clsx( + "group p-6 rounded-xl transition-all duration-200 cursor-pointer", + "bg-white dark:bg-gray-900", + "border border-gray-200 dark:border-gray-700", + "shadow-md dark:shadow-gray-900/20", + "hover:shadow-lg dark:hover:shadow-gray-900/30", + "hover:border-blue-300 dark:hover:border-blue-600", + "hover:bg-gray-50/50 dark:hover:bg-gray-800/50", + "hover:-translate-y-1", + "min-h-[200px] flex flex-col" + ), +}; + +export function ServerCard({ + serverName, + serverUrl, + workflowTools, + onSelectServer, + onDeleteServer, +}: ServerCardProps) { + const [tools, setTools] = useState[]>([]); + const [toolsLoading, setToolsLoading] = useState(true); + const [toolsError, setToolsError] = useState(null); + + // Fetch tools on mount + useEffect(() => { + const fetchServerTools = async () => { + setToolsLoading(true); + setToolsError(null); + try { + const fetched = await fetchTools(serverUrl, serverName); + setTools(fetched); + } catch (err: any) { + setToolsError(err?.message || 'Failed to fetch tools'); + setTools([]); + } finally { + setToolsLoading(false); + } + }; + + fetchServerTools(); + }, [serverUrl, serverName]); + + const handleCardClick = useCallback(() => { + onSelectServer(); + }, [onSelectServer]); + + // Calculate selected tools count for this server + const selectedToolsCount = workflowTools + .filter(tool => tool.isMcp && tool.mcpServerName === serverName) + .length; + + return ( +
+
+ {/* Header */} +
+
+ +
+
+

+ {serverName} +

+
+ {toolsLoading ? ( + + Loading tools... + + ) : toolsError ? ( + + Error loading tools + + ) : ( + + {selectedToolsCount > 0 + ? `${tools.length} tools, ${selectedToolsCount} selected` + : `${tools.length} tools` + } + + )} +
+
+ + + + + + } + onPress={onDeleteServer} + > + Delete + + + +
+ + {/* Description */} +
+

+ Custom MCP server at {serverUrl} +

+
+ + {/* Footer */} +
+
+
+ + Custom Server + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx index a0147ac3..f6457c30 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx @@ -5,12 +5,11 @@ import { PictureImg } from '@/components/ui/picture-img'; import clsx from 'clsx'; import { z } from 'zod'; import { ZToolkit } from '@/app/lib/composio/composio'; -import { Project } from '@/app/lib/types/project_types'; import { Chip } from '@heroui/react'; import { LinkIcon } from 'lucide-react'; +import { Workflow } from '@/app/lib/types/workflow_types'; type ToolkitType = z.infer; -type ProjectType = z.infer; const toolkitCardStyles = { base: clsx( @@ -28,32 +27,25 @@ const toolkitCardStyles = { interface ToolkitCardProps { toolkit: ToolkitType; - projectId: string; isConnected: boolean; - connectedAccountId?: string; - projectConfig: ProjectType | null; - onManageTools: () => void; - onProjectConfigUpdate: () => void; - onRemoveToolkitTools: (toolkitSlug: string) => void; + onSelectToolkit: () => void; + workflowTools: z.infer; } export function ToolkitCard({ toolkit, - projectId, isConnected, - connectedAccountId, - projectConfig, - onManageTools, - onProjectConfigUpdate, - onRemoveToolkitTools + onSelectToolkit, + workflowTools, }: ToolkitCardProps) { const handleCardClick = useCallback(() => { - onManageTools(); - }, [onManageTools]); + onSelectToolkit(); + }, [onSelectToolkit]); // Calculate selected tools count for this toolkit - // TODO: Update to use workflow-based tools count - const selectedToolsCount = 0; + const selectedToolsCount = workflowTools + .filter(tool => tool.isComposio && tool.composioData?.toolkitSlug === toolkit.slug) + .length; return (
diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx index aa4f1128..0a063aff 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx @@ -2,23 +2,25 @@ import { useState } from 'react'; import { Tabs, Tab } from '@/components/ui/tabs'; -import { HostedServers } from './HostedServers'; -import { CustomServers } from './CustomServers'; -import { WebhookConfig } from './WebhookConfig'; +import { CustomMcpServers } from './CustomMcpServer'; import { Composio } from './Composio'; +import { AddWebhookTool } from './AddWebhookTool'; import type { Key } from 'react'; +import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; +import { z } from 'zod'; export function ToolsConfig({ + projectId, useComposioTools, - useKlavisTools + tools, + onAddTool, }: { + projectId: string; useComposioTools: boolean; - useKlavisTools: boolean; + tools: z.infer; + onAddTool: (tool: Partial>) => void; }) { - let defaultActiveTab = 'custom'; - if (useKlavisTools) { - defaultActiveTab = 'hosted'; - } + let defaultActiveTab = 'mcp'; if (useComposioTools) { defaultActiveTab = 'composio'; } @@ -40,32 +42,28 @@ export function ToolsConfig({ {useComposioTools && (
- +
)} - {useKlavisTools && ( - - Klavis - - BETA - -
- }> -
- setActiveTab(key)} /> -
- - )} - +
- +
- +
diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx index edc82b4a..577dfac4 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx @@ -1,42 +1,20 @@ 'use client'; import { useState, useEffect } from "react"; -import { useParams } from 'next/navigation'; -import { Spinner } from "@heroui/react"; +import { Spinner, Button, Input } from "@heroui/react"; import { getProjectConfig, updateWebhookUrl } from "@/app/actions/project_actions"; -import { Textarea } from "@/components/ui/textarea"; import { clsx } from "clsx"; +import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal'; -const sectionHeaderStyles = "block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2"; -const sectionDescriptionStyles = "text-sm text-gray-500 dark:text-gray-400 mb-4"; -const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500"; -const inputStyles = "rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20"; - -function Section({ title, children, description }: { - title: string; - children: React.ReactNode; - description?: string; -}) { - return ( -
-
-

{title}

- {description && ( -

{description}

- )} -
-
{children}
-
- ); -} - -export function WebhookConfig() { - const params = useParams(); - const projectId = params.projectId ? (typeof params.projectId === 'string' ? params.projectId : params.projectId[0]) : ''; +export function WebhookConfig({ projectId }: { projectId: string }) { const [loading, setLoading] = useState(true); const [webhookUrl, setWebhookUrl] = useState(null); const [error, setError] = useState(null); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [saving, setSaving] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [editValue, setEditValue] = useState(''); useEffect(() => { let mounted = true; @@ -46,6 +24,7 @@ export function WebhookConfig() { const project = await getProjectConfig(projectId); if (mounted) { setWebhookUrl(project.webhookUrl || null); + setEditValue(project.webhookUrl || ''); setError(null); } } catch (err) { @@ -67,59 +46,138 @@ export function WebhookConfig() { }; }, [projectId]); - function validate(url: string) { - if (!url.trim()) { - return { valid: true }; - } + // validate on change in webhook + useEffect(() => { + if (!isEditMode) return; + + setError(null); try { - new URL(url); - setError(null); - return { valid: true }; + new URL(editValue || ''); } catch { setError('Please enter a valid URL'); - return { valid: false, errorMessage: 'Please enter a valid URL' }; + } + }, [editValue, isEditMode]); + + const handleEdit = () => { + setIsEditMode(true); + setEditValue(webhookUrl || ''); + setError(null); + }; + + const handleCancel = () => { + setIsEditMode(false); + setEditValue(webhookUrl || ''); + setError(null); + }; + + async function handleSave() { + setSaving(true); + try { + await updateWebhookUrl(projectId, editValue); + setWebhookUrl(editValue); + setIsEditMode(false); + setShowConfirmModal(false); + } catch (err) { + console.error('Failed to update webhook URL:', err); + setError('Failed to update webhook URL'); + } finally { + setSaving(false); } } - return ( -
-
-
-
-