housekeeping

This commit is contained in:
Ramnique Singh 2025-08-17 10:51:54 +05:30
parent 3755e76474
commit cc8e9210d7
4 changed files with 0 additions and 1130 deletions

View file

@ -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<z.infer<typeof WorkflowTool>[]> {
await projectAuthCheck(projectId);
const project = await projectsCollection.findOne({
_id: projectId,
});
const mcpServers = project?.mcpServers ?? [];
const tools: z.infer<typeof WorkflowTool>[] = [];
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<typeof McpServerTool> =>
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<z.infer<typeof WorkflowTool>[]> {
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<typeof WorkflowTool>[] = [];
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<typeof McpServerTool> => 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<typeof Project>['mcpServers']): Promise<void> {
await projectAuthCheck(projectId);
await projectsCollection.updateOne({
_id: projectId,
}, { $set: { mcpServers } });
}
export async function toggleMcpTool(
projectId: string,
serverName: string,
toolId: string,
shouldAdd: boolean
): Promise<void> {
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<string[]> {
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<string, any>
): Promise<any> {
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;
}
}

View file

@ -9,7 +9,6 @@ import { Info, RefreshCw, RefreshCcw, Lock, Wrench } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { MCPServer, McpTool } from '@/app/lib/types/types'; import { MCPServer, McpTool } from '@/app/lib/types/types';
import type { z } from 'zod'; import type { z } from 'zod';
import { TestToolModal } from './TestToolModal';
type McpServerType = z.infer<typeof MCPServer>; type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof McpTool>; type McpToolType = z.infer<typeof McpTool>;
@ -484,15 +483,6 @@ export function ToolManagementPanel({
</div> </div>
</div> </div>
</SlidePanel> </SlidePanel>
{testingTool && (
<TestToolModal
isOpen={!!testingTool}
onClose={() => setTestingTool(null)}
tool={testingTool}
server={server}
/>
)}
</> </>
); );
} }

View file

@ -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<typeof MCPServer>;
type McpToolType = z.infer<typeof McpTool>;
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<Record<string, any>>({});
const [response, setResponse] = useState<any>(null);
const [error, setError] = useState<string | null>(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<string | null>(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 (
<div className="space-y-2">
{arrayValue.map((item: any, index: number) => (
<div key={index} className="flex gap-2 items-start">
<div className="text-sm text-gray-500 dark:text-gray-400 pt-2 min-w-[24px]">
{index + 1}:
</div>
<div className="flex-1">
{itemSchema.type === 'string' ? (
<Input
type="text"
value={item || ''}
onChange={(e) => 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' ? (
<Input
type="number"
value={item || ''}
step={itemSchema.type === 'integer' ? '1' : 'any'}
min={itemSchema.minimum}
max={itemSchema.maximum}
onChange={(e) => {
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' ? (
<div className="scale-75 origin-left">
<Switch
checked={!!item}
onCheckedChange={(checked) => handleArrayItemChange(index, checked)}
/>
</div>
) : (
<div className="w-full">
{renderParameterInput(paramName, {
...itemSchema,
value: item,
onChange: (newValue: any) => handleArrayItemChange(index, newValue)
})}
</div>
)}
</div>
<Button
variant="secondary"
onClick={() => {
const newArray = arrayValue.filter((_, i) => i !== index);
handleParameterChange(paramName, newArray);
}}
className="px-2 h-9 hover:bg-transparent border-transparent"
>
<Trash2 className="h-4 w-4 text-gray-500 hover:text-red-500 transition-colors" />
</Button>
</div>
))}
<Button
variant="secondary"
onClick={() => {
const defaultValue = itemSchema.type === 'object' ? {} :
itemSchema.type === 'array' ? [] :
itemSchema.type === 'boolean' ? false : '';
handleParameterChange(paramName, [...arrayValue, defaultValue]);
}}
className="text-xs text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-transparent border-transparent"
>
Add item
</Button>
</div>
);
case 'object':
if (!schema.properties) return null;
const objectValue = typeof value === 'object' ? value : {};
return (
<div className="space-y-4 border-l-2 border-gray-200 dark:border-gray-700 pl-4 mt-2">
{Object.entries(schema.properties).map(([key, propSchema]: [string, any]) => (
<div key={key} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{key}
{schema.required?.includes(key) && <span className="text-red-500 ml-1">*</span>}
</label>
{renderParameterInput(
`${paramName}.${key}`,
propSchema
)}
</div>
))}
</div>
);
case 'string':
if (schema.enum) {
return (
<select
value={value}
onChange={(e) => handleParameterChange(paramName, e.target.value)}
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 hover:border-gray-300 dark:hover:border-gray-600
focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0!"
>
<option value="" disabled>Select {paramName}</option>
{schema.enum.map((opt: string) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
}
if (schema.format === 'date-time') {
return (
<Input
type="datetime-local"
value={value}
onChange={(e) => 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 (
<Input
type="date"
value={value}
onChange={(e) => 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 (
<Input
type="time"
value={value}
onChange={(e) => handleParameterChange(paramName, e.target.value)}
className="focus:ring-0 focus:ring-offset-0 ring-0! ring-offset-0! focus:outline-none"
/>
);
}
return (
<Input
type="text"
value={value}
onChange={(e) => 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 (
<Input
type="number"
value={value}
step={schema.type === 'integer' ? '1' : 'any'}
min={schema.minimum}
max={schema.maximum}
onChange={(e) => {
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 (
<div className="scale-75 origin-left">
<Switch
checked={!!value}
onCheckedChange={(checked) => handleParameterChange(paramName, checked)}
/>
</div>
);
case 'null':
return (
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
Null value
</div>
);
default:
return (
<Input
type="text"
value={value}
onChange={(e) => 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-hidden">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-gray-900 rounded-lg shadow-xl w-[900px] max-w-[90vw] max-h-[90vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Test {tool.name}
</h2>
<button
onClick={onClose}
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<X className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto min-h-0 p-6 space-y-6">
<div className="text-sm text-gray-500 dark:text-gray-400">
{tool.description}
</div>
{validationError && (
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-3 rounded-md mb-6">
{validationError}
</div>
)}
<div className="flex flex-col flex-1 min-h-0">
<div>
<button
onClick={() => setShowInputs(!showInputs)}
className="flex items-center gap-1 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
>
{showInputs ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
Inputs
</button>
{showInputs && (
<div className="space-y-6 pl-5 mt-4">
<div className="flex flex-col gap-2 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<div className="scale-75 origin-left">
<Switch
checked={showOnlyRequired}
onCheckedChange={setShowOnlyRequired}
/>
</div>
<label className="text-sm text-gray-700 dark:text-gray-300">
Show only required parameters
</label>
</div>
<div className="flex items-center gap-2">
<div className="scale-75 origin-left">
<Switch
checked={showDescriptions}
onCheckedChange={setShowDescriptions}
/>
</div>
<label className="text-sm text-gray-700 dark:text-gray-300">
Show parameter descriptions
</label>
</div>
</div>
{getFilteredParameters().map(([name, schema]) => (
<div key={name} className="space-y-3">
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{name}
{tool.parameters?.required?.includes(name) && (
<span className="text-red-500 ml-1">*</span>
)}
</label>
{showDescriptions && schema.description && (
<p className="text-xs text-gray-600 dark:text-gray-400">
{schema.description}
</p>
)}
</div>
{renderParameterInput(name, schema)}
</div>
))}
</div>
)}
</div>
{error && (
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-3 rounded-md mt-6">
{error}
</div>
)}
<div className="mt-6">
<div className="flex items-center justify-between">
<button
onClick={() => setShowRequest(!showRequest)}
className="flex items-center gap-1 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
>
{showRequest ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
Request
</button>
{showRequest && (
<button
onClick={() => handleCopy('request')}
className="p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center gap-1.5"
title="Copy request"
>
<Copy className="h-4 w-4 text-gray-500 dark:text-gray-400" />
{copySuccess === 'request' && (
<span className="text-xs text-green-600 dark:text-green-400">
Copied!
</span>
)}
</button>
)}
</div>
{showRequest && (
<div className="pl-5 mt-4">
<pre className="text-sm bg-gray-50 dark:bg-gray-800 p-3 rounded-md overflow-auto max-h-60">
{JSON.stringify({ name: tool.id, arguments: parameters }, null, 2)}
</pre>
</div>
)}
</div>
{/* Response section - shown when loading or when there's a response */}
{(isLoading || response) && (
<div className="flex flex-col flex-1 min-h-0 mt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Response</h4>
{response && (
<>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600 dark:text-gray-400">
Raw
</label>
<div className="scale-75 origin-right">
<Switch
checked={showRawResponse}
onCheckedChange={setShowRawResponse}
/>
</div>
</div>
</>
)}
</div>
{response && (
<button
onClick={() => handleCopy('response')}
className="p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center gap-1.5"
title="Copy response"
>
<Copy className="h-4 w-4 text-gray-500 dark:text-gray-400" />
{copySuccess === 'response' && (
<span className="text-xs text-green-600 dark:text-green-400">
Copied!
</span>
)}
</button>
)}
</div>
<div className="pl-5 mt-4 flex-1 min-h-0">
{isLoading ? (
<div className="h-full bg-gray-50 dark:bg-gray-800 rounded-md p-3 flex items-start">
<div className="flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 dark:border-gray-600 border-t-blue-600 dark:border-t-blue-400" />
Awaiting response...
</div>
</div>
) : (
<>
<pre
className={clsx(
"text-sm bg-gray-50 dark:bg-gray-800 p-3 rounded-md overflow-auto h-full",
!showRawResponse && "whitespace-pre-wrap break-all"
)}
>
{formatResponse(response)}
</pre>
</>
)}
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-800">
<Button
variant="secondary"
onClick={handleReset}
disabled={isLoading}
className="text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20
border-red-200 dark:border-red-800 hover:border-red-300 dark:hover:border-red-700"
>
<span className="text-sm">Reset</span>
</Button>
<Button
variant="primary"
onClick={handleTest}
disabled={isLoading}
>
<span className="text-sm">{isLoading ? 'Awaiting...' : 'Test'}</span>
</Button>
</div>
</div>
</div>
);
}

View file

@ -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<typeof WorkflowTool>[]) => void;
}
export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tools, setTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
const [selectedTools, setSelectedTools] = useState<Set<number>>(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 (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="xl">
<ModalContent>
{(onClose) => (
<>
<ModalHeader>Import from MCP servers</ModalHeader>
<ModalBody>
{loading && <div className="flex gap-2 items-center">
<Spinner size="sm" />
Fetching tools...
</div>}
{error && <div className="bg-red-100 p-2 rounded-md text-red-800 flex items-center gap-2 text-sm">
{error}
<Button size="sm" color="danger" onPress={() => process()}>Retry</Button>
</div>}
{!loading && !error && <>
<div className="flex items-center justify-between mb-4">
<div className="text-gray-600">
{tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`}
</div>
<Button
size="sm"
variant="flat"
onPress={() => {
setTools([]);
process();
}}
startContent={<RefreshCwIcon className="w-4 h-4" />}
>
Refresh
</Button>
</div>
{tools.length > 0 && <div className="flex flex-col w-full mt-4">
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 rounded-t-lg border-b text-sm text-gray-700 font-medium">
<div className="w-8">
<Checkbox
size="sm"
isSelected={selectedTools.size === tools.length}
isIndeterminate={selectedTools.size > 0 && selectedTools.size < tools.length}
onValueChange={(checked) => {
if (checked) {
setSelectedTools(new Set(tools.map((_, i) => i)));
} else {
setSelectedTools(new Set());
}
}}
/>
</div>
<div className="w-36">Server</div>
<div className="flex-1">Tool Name</div>
</div>
<div className="border rounded-b-lg divide-y overflow-y-auto max-h-[300px]">
{tools.map((t, index) => (
<div
key={index}
className="flex items-center gap-4 px-4 py-2 hover:bg-gray-50 transition-colors"
>
<div className="w-8">
<Checkbox
size="sm"
isSelected={selectedTools.has(index)}
onValueChange={(checked) => {
const newSelected = new Set(selectedTools);
if (checked) {
newSelected.add(index);
} else {
newSelected.delete(index);
}
setSelectedTools(newSelected);
}}
/>
</div>
<div className="w-36">
<div className="bg-blue-50 px-2 py-1 rounded text-blue-700 text-sm font-medium border border-blue-100">
{t.mcpServerName}
</div>
</div>
<div className="flex-1 truncate text-gray-700">{t.name}</div>
</div>
))}
</div>
</div>}
{tools.length > 0 && (
<div className="mt-4 text-sm text-gray-600">
{selectedTools.size} of {tools.length} tools selected
</div>
)}
</>}
</ModalBody>
<ModalFooter>
<Button size="sm" variant="flat" onPress={onClose}>
Cancel
</Button>
{tools.length > 0 && <Button size="sm" onPress={() => {
const selectedToolsList = tools.filter((_, index) => selectedTools.has(index));
onImport(selectedToolsList);
onClose();
}}>
Import
</Button>}
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
}