mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
housekeeping
This commit is contained in:
parent
3755e76474
commit
cc8e9210d7
4 changed files with 0 additions and 1130 deletions
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue