From cc8e9210d7082a775e60f633850abb9d6eb127a4 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sun, 17 Aug 2025 10:51:54 +0530 Subject: [PATCH] housekeeping --- apps/rowboat/app/actions/mcp.actions.ts | 295 -------- .../tools/components/MCPServersCommon.tsx | 10 - .../tools/components/TestToolModal.tsx | 674 ------------------ .../[projectId]/workflow/mcp_imports.tsx | 151 ---- 4 files changed, 1130 deletions(-) delete mode 100644 apps/rowboat/app/actions/mcp.actions.ts delete mode 100644 apps/rowboat/app/projects/[projectId]/tools/components/TestToolModal.tsx delete mode 100644 apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx diff --git a/apps/rowboat/app/actions/mcp.actions.ts b/apps/rowboat/app/actions/mcp.actions.ts deleted file mode 100644 index b06af2b4..00000000 --- a/apps/rowboat/app/actions/mcp.actions.ts +++ /dev/null @@ -1,295 +0,0 @@ -"use server"; -import { z } from "zod"; -import { WorkflowTool } from "../lib/types/workflow_types"; -import { projectAuthCheck } from "./project.actions"; -import { projectsCollection } from "../lib/mongodb"; -import { Project } from "../lib/types/project_types"; -import { McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types"; -import { getMcpClient } from "../lib/mcp"; - -export async function fetchMcpTools(projectId: string): Promise[]> { - await projectAuthCheck(projectId); - - const project = await projectsCollection.findOne({ - _id: projectId, - }); - - const mcpServers = project?.mcpServers ?? []; - const tools: z.infer[] = []; - - for (const mcpServer of mcpServers) { - if (!mcpServer.isActive) continue; - - try { - const client = await getMcpClient(mcpServer.serverUrl!, mcpServer.name); - - // List tools - const result = await client.listTools(); - - // Validate and parse each tool - const validTools = await Promise.all( - result.tools.map(async (tool) => { - try { - return McpServerTool.parse(tool); - } catch (error) { - console.error(`Invalid tool response from ${mcpServer.name}:`, { - tool: tool.name, - error: error instanceof Error ? error.message : 'Unknown error' - }); - return null; - } - }) - ); - - // Filter out invalid tools and convert valid ones - tools.push(...validTools - .filter((tool): tool is z.infer => - tool !== null && - mcpServer.tools.some(t => t.id === tool.name) - ) - .map(mcpTool => convertMcpServerToolToWorkflowTool(mcpTool, mcpServer)) - ); - } catch (e) { - console.error(`Error fetching MCP tools from ${mcpServer.name}:`, { - error: e instanceof Error ? e.message : 'Unknown error', - serverUrl: mcpServer.serverUrl - }); - } - } - - return tools; -} - -export async function fetchMcpToolsForServer(projectId: string, serverName: string): Promise[]> { - await projectAuthCheck(projectId); - - console.log('[Klavis API] Fetching tools for specific server:', { projectId, serverName }); - - const project = await projectsCollection.findOne({ - _id: projectId, - }); - - const mcpServer = project?.mcpServers?.find(server => server.name === serverName); - if (!mcpServer) { - console.error('[Klavis API] Server not found:', { serverName }); - return []; - } - - if (!mcpServer.isActive || !mcpServer.serverUrl) { - console.log('[Klavis API] Server is not active or missing URL:', { - serverName, - isActive: mcpServer.isActive, - hasUrl: !!mcpServer.serverUrl - }); - return []; - } - - const tools: z.infer[] = []; - - try { - console.log('[Klavis API] Attempting MCP connection:', { - serverName, - url: mcpServer.serverUrl - }); - - const client = await getMcpClient(mcpServer.serverUrl, mcpServer.name); - - // List tools - const result = await client.listTools(); - - // Log just essential info about tools - console.log('[Klavis API] Received tools from server:', { - serverName, - toolCount: result.tools.length, - tools: result.tools.map(tool => tool.name).join(', ') - }); - - // Get all available tools from the server - const availableToolNames = new Set(mcpServer.availableTools?.map(t => t.name) || []); - - // Validate and parse each tool - const validTools = await Promise.all( - result.tools.map(async (tool) => { - try { - const parsedTool = McpServerTool.parse(tool); - return parsedTool; - } catch (error) { - console.error(`Invalid tool response from ${mcpServer.name}:`, { - tool: tool.name, - error: error instanceof Error ? error.message : 'Unknown error' - }); - return null; - } - }) - ); - - // Filter out invalid tools and convert valid ones - const convertedTools = validTools - .filter((tool): tool is z.infer => tool !== null) - .map(mcpTool => { - const converted = convertMcpServerToolToWorkflowTool(mcpTool, mcpServer); - return converted; - }); - - tools.push(...convertedTools); - - // Find tools that weren't enriched - const enrichedToolNames = new Set(convertedTools.map(t => t.name)); - const unenrichedTools = Array.from(availableToolNames).filter(name => !enrichedToolNames.has(name)); - - if (unenrichedTools.length > 0) { - console.log('[Klavis API] Tools that could not be enriched:', { - serverName, - unenrichedTools, - totalAvailable: availableToolNames.size, - totalEnriched: enrichedToolNames.size - }); - } - - console.log('[Klavis API] Successfully fetched tools for server:', { - serverName, - toolCount: tools.length, - availableToolCount: availableToolNames.size, - tools: tools.map(t => t.name).join(', ') - }); - } catch (e) { - console.error(`[Klavis API] Error fetching MCP tools from ${mcpServer.name}:`, { - error: e instanceof Error ? e.message : 'Unknown error', - serverUrl: mcpServer.serverUrl - }); - } - - return tools; -} - -export async function updateMcpServers(projectId: string, mcpServers: z.infer['mcpServers']): Promise { - await projectAuthCheck(projectId); - await projectsCollection.updateOne({ - _id: projectId, - }, { $set: { mcpServers } }); -} - -export async function toggleMcpTool( - projectId: string, - serverName: string, - toolId: string, - shouldAdd: boolean -): Promise { - await projectAuthCheck(projectId); - - // 1. Get the project and find the server - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) throw new Error("Project not found"); - - const mcpServers = project.mcpServers || []; - const serverIndex = mcpServers.findIndex(s => s.serverName === serverName); - if (serverIndex === -1) throw new Error("Server not found"); - - const server = mcpServers[serverIndex]; - - if (shouldAdd) { - // Add tool if it doesn't exist - const toolExists = server.tools.some(t => t.id === toolId); - if (!toolExists) { - // Find the tool in availableTools to get its parameters - const availableTool = server.availableTools?.find(t => t.name === toolId); - - // Create a new tool with the parameters from availableTools - const newTool = { - id: toolId, - name: toolId, - description: availableTool?.description || '', - parameters: availableTool?.parameters || { - type: 'object' as const, - properties: {}, - required: [] - } - }; - server.tools.push(newTool); - } - } else { - // Remove tool if it exists - server.tools = server.tools.filter(t => t.id !== toolId); - } - - // Update the project - await projectsCollection.updateOne( - { _id: projectId }, - { $set: { mcpServers } } - ); -} - -export async function getSelectedMcpTools(projectId: string, serverName: string): Promise { - await projectAuthCheck(projectId); - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) return []; - - const server = project.mcpServers?.find(s => s.serverName === serverName); - if (!server) return []; - - return server.tools.map(t => t.id); -} - -export async function testMcpTool( - projectId: string, - serverName: string, - toolId: string, - parameters: Record -): Promise { - await projectAuthCheck(projectId); - - const project = await projectsCollection.findOne({ - _id: projectId, - }); - - // Find the server by name in mcpServers array - const mcpServer = project?.mcpServers?.find(server => server.name === serverName); - if (!mcpServer) { - throw new Error(`Server ${serverName} not found`); - } - - if (!mcpServer.isActive) { - throw new Error(`Server ${serverName} is not active`); - } - - if (!mcpServer.serverUrl) { - throw new Error(`Server ${serverName} has no URL configured`); - } - - try { - console.log('[MCP Test] Attempting to test tool:', { - serverName, - serverUrl: mcpServer.serverUrl, - toolId - }); - - const client = await getMcpClient(mcpServer.serverUrl, mcpServer.name); - - console.log('[MCP Test] Connected to server, calling tool:', { - toolId, - parameters - }); - - // Execute the tool with the correct parameter format - const result = await client.callTool({ - name: toolId, - arguments: parameters - }); - - console.log('[MCP Test] Tool execution completed:', { - toolId, - success: true - }); - - return result; - - } catch (e) { - console.error(`[MCP Test] Error testing tool from ${mcpServer.name}:`, { - error: e instanceof Error ? e.message : 'Unknown error', - serverUrl: mcpServer.serverUrl, - toolId, - parameters - }); - throw e; - } -} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/MCPServersCommon.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/MCPServersCommon.tsx index 152b4fa3..f3119ac7 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/MCPServersCommon.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/MCPServersCommon.tsx @@ -9,7 +9,6 @@ import { Info, RefreshCw, RefreshCcw, Lock, Wrench } from 'lucide-react'; import { clsx } from 'clsx'; import { MCPServer, McpTool } from '@/app/lib/types/types'; import type { z } from 'zod'; -import { TestToolModal } from './TestToolModal'; type McpServerType = z.infer; type McpToolType = z.infer; @@ -484,15 +483,6 @@ export function ToolManagementPanel({ - - {testingTool && ( - setTestingTool(null)} - tool={testingTool} - server={server} - /> - )} ); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/TestToolModal.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/TestToolModal.tsx deleted file mode 100644 index 05294877..00000000 --- a/apps/rowboat/app/projects/[projectId]/tools/components/TestToolModal.tsx +++ /dev/null @@ -1,674 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Switch } from '@/components/ui/switch'; -import { MCPServer, McpTool } from '@/app/lib/types/types'; -import { testMcpTool } from '@/app/actions/mcp.actions'; -import { Copy, ChevronDown, ChevronRight, X, Trash2 } from 'lucide-react'; -import type { z } from 'zod'; -import clsx from 'clsx'; - -type McpServerType = z.infer; -type McpToolType = z.infer; - -interface TestToolModalProps { - isOpen: boolean; - onClose: () => void; - tool: McpToolType; - server: McpServerType; -} - -export function TestToolModal({ isOpen, onClose, tool, server }: TestToolModalProps) { - const params = useParams(); - const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; - if (!projectId) throw new Error('Project ID is required'); - - // Prevent body scroll when modal is open - useEffect(() => { - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - return () => { - document.body.style.overflow = 'unset'; - }; - }, [isOpen]); - - // Handle escape key - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - window.addEventListener('keydown', handleEscape); - return () => window.removeEventListener('keydown', handleEscape); - }, [onClose]); - - const [parameters, setParameters] = useState>({}); - const [response, setResponse] = useState(null); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [showRequest, setShowRequest] = useState(false); - const [showInputs, setShowInputs] = useState(true); - const [copySuccess, setCopySuccess] = useState<'request' | 'response' | null>(null); - const [showOnlyRequired, setShowOnlyRequired] = useState(false); - const [showDescriptions, setShowDescriptions] = useState(true); - const [validationError, setValidationError] = useState(null); - const [showRawResponse, setShowRawResponse] = useState(false); - - const handleReset = () => { - setParameters({}); - setResponse(null); - setError(null); - setShowRequest(false); - setShowInputs(true); - setValidationError(null); - setShowRawResponse(false); - }; - - const handleParameterChange = (name: string, value: any) => { - // Handle nested object updates - if (name.includes('.')) { - const parts = name.split('.'); - const topLevel = parts[0]; - const rest = parts.slice(1); - - setParameters(prev => { - const current = prev[topLevel] || {}; - let temp = current; - for (let i = 0; i < rest.length - 1; i++) { - temp[rest[i]] = temp[rest[i]] || {}; - temp = temp[rest[i]]; - } - temp[rest[rest.length - 1]] = value; - - return { - ...prev, - [topLevel]: current - }; - }); - } - // Handle array index updates - else if (name.includes('[') && name.includes(']')) { - const matches = name.match(/^([^\[]+)\[(\d+)\]$/); - if (matches) { - const [_, arrayName, index] = matches; - setParameters(prev => { - const array = Array.isArray(prev[arrayName]) ? [...prev[arrayName]] : []; - array[parseInt(index, 10)] = value; - return { - ...prev, - [arrayName]: array - }; - }); - } - } - // Handle regular updates - else { - setParameters(prev => ({ - ...prev, - [name]: value - })); - } - setValidationError(null); - }; - - const validateRequiredParameters = () => { - const missingParams = tool.parameters?.required?.filter(param => { - const value = parameters[param]; - return value === undefined || value === null || value === ''; - }) || []; - - if (missingParams.length > 0) { - setValidationError(`Please fill in all required parameters: ${missingParams.join(', ')}`); - return false; - } - return true; - }; - - const handleCopy = async (type: 'request' | 'response') => { - const textToCopy = type === 'request' - ? JSON.stringify({ name: tool.id, arguments: parameters }, null, 2) - : (typeof response === 'string' ? response : JSON.stringify(response, null, 2)); - - try { - await navigator.clipboard.writeText(textToCopy); - setCopySuccess(type); - setTimeout(() => setCopySuccess(null), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - const handleTest = async () => { - setValidationError(null); - if (!validateRequiredParameters()) return; - - // Collapse both sections - setShowInputs(false); - setShowRequest(false); - - setResponse(null); - setError(null); - setIsLoading(true); - - try { - const result = await testMcpTool(projectId, server.name, tool.id, parameters); - setResponse(result); - } catch (e) { - setError(e instanceof Error ? e.message : 'An error occurred while testing the tool'); - } finally { - setIsLoading(false); - } - }; - - const renderParameterInput = (paramName: string, schema: any) => { - const value = parameters[paramName] ?? (schema.type === 'array' ? [] : schema.type === 'object' ? {} : ''); - - switch (schema.type) { - case 'array': - const arrayValue = Array.isArray(value) ? value : value ? [value] : []; - const itemSchema = schema.items || { type: 'string' }; - - const handleArrayItemChange = (index: number, itemValue: any) => { - const newArray = [...arrayValue]; - newArray[index] = itemValue; - handleParameterChange(paramName, newArray); - }; - - return ( -
- {arrayValue.map((item: any, index: number) => ( -
-
- {index + 1}: -
-
- {itemSchema.type === 'string' ? ( - handleArrayItemChange(index, e.target.value)} - placeholder="Enter value" - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ) : itemSchema.type === 'number' || itemSchema.type === 'integer' ? ( - { - const val = itemSchema.type === 'integer' ? - parseInt(e.target.value, 10) : - parseFloat(e.target.value); - handleArrayItemChange(index, isNaN(val) ? '' : val); - }} - placeholder="Enter value" - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ) : itemSchema.type === 'boolean' ? ( -
- handleArrayItemChange(index, checked)} - /> -
- ) : ( -
- {renderParameterInput(paramName, { - ...itemSchema, - value: item, - onChange: (newValue: any) => handleArrayItemChange(index, newValue) - })} -
- )} -
- -
- ))} - -
- ); - - case 'object': - if (!schema.properties) return null; - const objectValue = typeof value === 'object' ? value : {}; - return ( -
- {Object.entries(schema.properties).map(([key, propSchema]: [string, any]) => ( -
- - {renderParameterInput( - `${paramName}.${key}`, - propSchema - )} -
- ))} -
- ); - - case 'string': - if (schema.enum) { - return ( - - ); - } - if (schema.format === 'date-time') { - return ( - handleParameterChange(paramName, e.target.value)} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - } - if (schema.format === 'date') { - return ( - handleParameterChange(paramName, e.target.value)} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - } - if (schema.format === 'time') { - return ( - handleParameterChange(paramName, e.target.value)} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - } - return ( - handleParameterChange(paramName, e.target.value)} - placeholder={`Enter ${paramName}`} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - - case 'number': - case 'integer': - return ( - { - const val = schema.type === 'integer' ? - parseInt(e.target.value, 10) : - parseFloat(e.target.value); - handleParameterChange(paramName, isNaN(val) ? '' : val); - }} - placeholder={`Enter ${paramName}`} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - - case 'boolean': - return ( -
- handleParameterChange(paramName, checked)} - /> -
- ); - - case 'null': - return ( -
- Null value -
- ); - - default: - return ( - handleParameterChange(paramName, e.target.value)} - placeholder={`Enter ${paramName}`} - className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none" - /> - ); - } - }; - - const getFilteredParameters = () => { - if (!tool.parameters?.properties) return []; - - return Object.entries(tool.parameters.properties).filter(([name]) => { - if (showOnlyRequired) { - return tool.parameters?.required?.includes(name); - } - return true; - }); - }; - - const formatResponse = (response: any): string => { - try { - if (showRawResponse) { - return typeof response === 'string' ? response : JSON.stringify(response); - } - - // Convert to object if it's a string - const obj = typeof response === 'string' ? JSON.parse(response) : response; - - // Handle nested structures and attempt to parse JSON strings - const processValue = (value: any): any => { - if (typeof value === 'string') { - try { - // Try to parse string as JSON if it looks like JSON - if ((value.startsWith('{') && value.endsWith('}')) || - (value.startsWith('[') && value.endsWith(']'))) { - const parsed = JSON.parse(value); - return processValue(parsed); // Recursively process the parsed JSON - } - } catch { - // Not valid JSON, treat as regular string - } - // Preserve explicit newlines in regular strings - return value; - } - if (Array.isArray(value)) { - return value.map(processValue); - } - if (value && typeof value === 'object') { - const processed: any = {}; - for (const [k, v] of Object.entries(value)) { - processed[k] = processValue(v); - } - return processed; - } - return value; - }; - - // Process and stringify with proper indentation - const processed = processValue(obj); - const stringified = JSON.stringify(processed, null, 2); - - // Replace escaped newlines and handle nested JSON formatting - return stringified - .replace(/\\n/g, '\n') // Convert escaped newlines to actual newlines - .replace(/"\{/g, '{') // Remove quotes around nested JSON objects - .replace(/\}"/g, '}') // Remove quotes around nested JSON objects - .replace(/"\[/g, '[') // Remove quotes around nested JSON arrays - .replace(/\]"/g, ']'); // Remove quotes around nested JSON arrays - } catch (e) { - // If JSON parsing fails, return as is - return String(response); - } - }; - - if (!isOpen) return null; - - return ( -
- {/* Backdrop */} -
- - {/* Modal */} -
- {/* Header */} -
-

- Test {tool.name} -

- -
- - {/* Content */} -
-
- {tool.description} -
- - {validationError && ( -
- {validationError} -
- )} - -
-
- - - {showInputs && ( -
-
-
-
- -
- -
-
-
- -
- -
-
- - {getFilteredParameters().map(([name, schema]) => ( -
-
- - {showDescriptions && schema.description && ( -

- {schema.description} -

- )} -
- {renderParameterInput(name, schema)} -
- ))} -
- )} -
- - {error && ( -
- {error} -
- )} - -
-
- - {showRequest && ( - - )} -
- - {showRequest && ( -
-
-                    {JSON.stringify({ name: tool.id, arguments: parameters }, null, 2)}
-                  
-
- )} -
- - {/* Response section - shown when loading or when there's a response */} - {(isLoading || response) && ( -
-
-
-

Response

- {response && ( - <> -
-
- -
- -
-
- - )} -
- {response && ( - - )} -
-
- {isLoading ? ( -
-
-
- Awaiting response... -
-
- ) : ( - <> -
-                        {formatResponse(response)}
-                      
- - )} -
-
- )} -
-
- - {/* Footer */} -
- - -
-
-
- ); -} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx b/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx deleted file mode 100644 index 476c52a7..00000000 --- a/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx +++ /dev/null @@ -1,151 +0,0 @@ -"use client"; -import { useCallback, useEffect, useState } from "react"; -import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Checkbox } from "@heroui/react"; -import { z } from "zod"; -import { WorkflowTool } from "@/app/lib/types/workflow_types"; -import { RefreshCwIcon } from "lucide-react"; -import { fetchMcpTools } from "@/app/actions/mcp.actions"; - -interface McpImportToolsProps { - projectId: string; - isOpen: boolean; - onOpenChange: (open: boolean) => void; - onImport: (tools: z.infer[]) => void; -} - -export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [tools, setTools] = useState[]>([]); - const [selectedTools, setSelectedTools] = useState>(new Set()); - - const process = useCallback(async () => { - setLoading(true); - setError(null); - setSelectedTools(new Set()); - try { - const result = await fetchMcpTools(projectId); - setTools(result); - // Select all tools by default - setSelectedTools(new Set(result.map((_, index) => index))); - } catch (error) { - setError(`Unable to fetch tools: ${error}`); - } finally { - setLoading(false); - } - }, [projectId]); - - useEffect(() => { - console.log("mcp import tools useEffect", isOpen); - if (isOpen) { - process(); - } - }, [isOpen, process]); - - return ( - - - {(onClose) => ( - <> - Import from MCP servers - - {loading &&
- - Fetching tools... -
} - {error &&
- {error} - -
} - {!loading && !error && <> -
-
- {tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`} -
- -
- {tools.length > 0 &&
-
-
- 0 && selectedTools.size < tools.length} - onValueChange={(checked) => { - if (checked) { - setSelectedTools(new Set(tools.map((_, i) => i))); - } else { - setSelectedTools(new Set()); - } - }} - /> -
-
Server
-
Tool Name
-
-
- {tools.map((t, index) => ( -
-
- { - const newSelected = new Set(selectedTools); - if (checked) { - newSelected.add(index); - } else { - newSelected.delete(index); - } - setSelectedTools(newSelected); - }} - /> -
-
-
- {t.mcpServerName} -
-
-
{t.name}
-
- ))} -
-
} - {tools.length > 0 && ( -
- {selectedTools.size} of {tools.length} tools selected -
- )} - } -
- - - {tools.length > 0 && } - - - )} -
-
- ); -} \ No newline at end of file