import React, { useState, useCallback, useRef } from 'react'; import { Link } from '@tanstack/react-router'; import { ArrowLeft, AlertTriangle, CheckCircle, PanelLeftClose, PanelLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import ConnectServerModal from '../ConnectServerModal'; import { ServersList } from './ServersList'; import { ToolsList } from './ToolsList'; import { ToolInputForm } from './ToolInputForm'; import { ToolResult } from './ToolResult'; import { ExecutionHistory, type ExecutionHistoryItem } from './ExecutionHistory'; import type { ToolResult as ToolResultType } from '@dexto/core'; import { cn } from '@/lib/utils'; import { client } from '@/lib/client'; import { useServers, useServerTools } from '../hooks/useServers'; import type { McpServer, McpTool } from '../hooks/useServers'; export default function PlaygroundView() { const [selectedServer, setSelectedServer] = useState(null); const [selectedTool, setSelectedTool] = useState(null); const [toolInputs, setToolInputs] = useState>({}); const [toolResult, setToolResult] = useState(null); const [currentError, setCurrentError] = useState(null); const [inputErrors, setInputErrors] = useState>({}); const [isConnectModalOpen, setIsConnectModalOpen] = useState(false); const [executionLoading, setExecutionLoading] = useState(false); const [executionHistory, setExecutionHistory] = useState([]); const [clipboardNotification, setClipboardNotification] = useState<{ message: string; type: 'success' | 'error'; } | null>(null); // Search states const [serverSearchQuery, setServerSearchQuery] = useState(''); const [toolSearchQuery, setToolSearchQuery] = useState(''); // Responsive sidebar states const [showServersSidebar, setShowServersSidebar] = useState(true); const [showToolsSidebar, setShowToolsSidebar] = useState(true); const executionAbortControllerRef = useRef(null); const { data: servers = [], isLoading: serversLoading, error: serversError, refetch: refetchServers, } = useServers(); const { data: tools = [], isLoading: toolsLoading, error: toolsError, } = useServerTools( selectedServer?.id || null, !!selectedServer && selectedServer.status === 'connected' ); const handleError = (message: string, area?: 'servers' | 'tools' | 'execution' | 'input') => { console.error(`Playground Error (${area || 'general'}):`, message); if (area !== 'input') { setCurrentError(message); } }; const handleServerSelect = useCallback((server: McpServer) => { setSelectedServer(server); setSelectedTool(null); setToolResult(null); setCurrentError(null); setInputErrors({}); }, []); const handleToolSelect = useCallback((tool: McpTool) => { setSelectedTool(tool); setToolResult(null); setCurrentError(null); setInputErrors({}); const defaultInputs: Record = {}; if (tool.inputSchema && tool.inputSchema.properties) { for (const key in tool.inputSchema.properties) { const prop = tool.inputSchema.properties[key]; if (prop.default !== undefined) { defaultInputs[key] = prop.default; } else { if (prop.type === 'boolean') defaultInputs[key] = false; else if (prop.type === 'number' || prop.type === 'integer') defaultInputs[key] = ''; else if (prop.type === 'object' || prop.type === 'array') defaultInputs[key] = ''; else defaultInputs[key] = ''; } } } setToolInputs(defaultInputs); }, []); const handleInputChange = useCallback( ( inputName: string, value: any, type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' ) => { setToolInputs((prev) => ({ ...prev, [inputName]: value })); if (inputErrors[inputName]) { setInputErrors((prev) => ({ ...prev, [inputName]: '' })); } if (type === 'object' || type === 'array') { if (value === '') return; try { JSON.parse(value); } catch { setInputErrors((prev) => ({ ...prev, [inputName]: 'Invalid JSON format' })); return; } } }, [inputErrors] ); const validateInputs = useCallback((): boolean => { if (!selectedTool || !selectedTool.inputSchema || !selectedTool.inputSchema.properties) { return true; } const currentInputErrors: Record = {}; let allValid = true; for (const key in selectedTool.inputSchema.properties) { const prop = selectedTool.inputSchema.properties[key]; const value = toolInputs[key]; if (selectedTool.inputSchema.required?.includes(key)) { if ( value === undefined || value === '' || (prop.type === 'boolean' && typeof value !== 'boolean') ) { currentInputErrors[key] = 'This field is required.'; allValid = false; continue; } } if ( (prop.type === 'number' || prop.type === 'integer') && value !== '' && isNaN(Number(value)) ) { currentInputErrors[key] = 'Must be a valid number.'; allValid = false; } if ((prop.type === 'object' || prop.type === 'array') && value !== '') { try { JSON.parse(value as string); } catch { currentInputErrors[key] = 'Invalid JSON format.'; allValid = false; } } } setInputErrors(currentInputErrors); return allValid; }, [selectedTool, toolInputs]); const handleExecuteTool = useCallback(async () => { if (!selectedServer || !selectedTool) { handleError('No server or tool selected for execution.', 'execution'); return; } executionAbortControllerRef.current?.abort(); const controller = new AbortController(); executionAbortControllerRef.current = controller; setCurrentError(null); setToolResult(null); if (!validateInputs()) { handleError('Please correct the input errors.', 'input'); return; } const executionStart = Date.now(); const executionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; setExecutionLoading(true); try { const processedInputs: Record = {}; if (selectedTool.inputSchema && selectedTool.inputSchema.properties) { for (const key in selectedTool.inputSchema.properties) { const prop = selectedTool.inputSchema.properties[key]; let value = toolInputs[key]; if (prop.type === 'number') { value = value === '' ? undefined : Number(value); } else if (prop.type === 'integer') { if (value === '') { value = undefined; } else { const num = Number(value); if (!Number.isInteger(num)) { setInputErrors((prev) => ({ ...prev, [key]: 'Must be a valid integer.', })); setExecutionLoading(false); return; } value = num; } } else if (prop.type === 'boolean') { if (typeof value === 'string') { value = value === 'true'; } else { value = Boolean(value); } } else if ( (prop.type === 'object' || prop.type === 'array') && typeof value === 'string' && value.trim() !== '' ) { try { value = JSON.parse(value); } catch { setInputErrors((prev) => ({ ...prev, [key]: 'Invalid JSON before sending.', })); setExecutionLoading(false); return; } } else if ( (prop.type === 'object' || prop.type === 'array') && (value === undefined || value === '') ) { value = undefined; } if (value !== undefined) { processedInputs[key] = value; } } } const response = await client.api.mcp.servers[':serverId'].tools[ ':toolName' ].execute.$post( { param: { serverId: selectedServer.id, toolName: selectedTool.id, }, json: processedInputs, }, { init: { signal: controller.signal } } ); if (!response.ok) { throw new Error('Tool execution failed'); } const resultData = await response.json(); const duration = Date.now() - executionStart; setToolResult(resultData); setExecutionHistory((prev) => [ { id: executionId, toolName: selectedTool.name, timestamp: new Date(), success: true, duration, }, ...prev.slice(0, 9), ]); } catch (err: any) { if (err.name !== 'AbortError') { const duration = Date.now() - executionStart; handleError(err.message, 'execution'); if ( err.message && (!toolResult || toolResult.success || toolResult.error !== err.message) ) { setToolResult({ success: false, error: err.message }); } setExecutionHistory((prev) => [ { id: executionId, toolName: selectedTool?.name || 'Unknown', timestamp: new Date(), success: false, duration, }, ...prev.slice(0, 9), ]); } } finally { if (!controller.signal.aborted) { setExecutionLoading(false); } } }, [selectedServer, selectedTool, toolInputs, validateInputs, toolResult]); const handleModalClose = () => { setIsConnectModalOpen(false); refetchServers(); }; const copyToClipboard = async (text: string, successMessage?: string) => { try { await navigator.clipboard.writeText(text); setClipboardNotification({ message: successMessage || 'Copied to clipboard', type: 'success', }); setTimeout(() => setClipboardNotification(null), 3000); } catch (err) { setClipboardNotification({ message: 'Failed to copy to clipboard. Please check browser permissions.', type: 'error', }); setTimeout(() => setClipboardNotification(null), 5000); console.error('Failed to copy to clipboard:', err); } }; const copyToolConfiguration = () => { if (!selectedTool || !selectedServer) return; const config = { server: selectedServer.name, tool: selectedTool.name, inputs: toolInputs, timestamp: new Date().toISOString(), }; copyToClipboard(JSON.stringify(config, null, 2), 'Tool configuration copied!'); }; const copyToolResult = () => { if (!toolResult) return; const resultText = typeof toolResult.data === 'object' ? JSON.stringify(toolResult.data, null, 2) : String(toolResult.data); copyToClipboard(resultText, 'Tool result copied!'); }; const shareToolConfig = () => { if (!selectedTool || !selectedServer) return; const shareText = `Check out this Dexto tool configuration:\n\nServer: ${selectedServer.name}\nTool: ${selectedTool.name}\nInputs: ${JSON.stringify(toolInputs, null, 2)}`; if (navigator.share) { navigator.share({ title: `Dexto Tool: ${selectedTool.name}`, text: shareText, }); } else { copyToClipboard(shareText, 'Tool configuration copied for sharing!'); } }; return (
{/* Servers Sidebar */} {/* Tools Sidebar */} {/* Main Content */}
{/* Header */}
{!showServersSidebar && ( )} {!showToolsSidebar && ( )}

Tool Runner

{/* Clipboard Notification */} {clipboardNotification && ( {clipboardNotification.type === 'error' && ( )} {clipboardNotification.type === 'success' && ( )} {clipboardNotification.message} )} {/* Error Display */} {currentError && selectedTool && (!toolResult || !toolResult.success) && (

Error:

{currentError}

)} {/* Empty State */} {!selectedTool && (

Select a Tool

Choose a tool from the left panel to start testing and experimenting with MCP capabilities.

)} {/* Tool Content */} {selectedTool && (
{/* Tool Info Card */}

{selectedTool.name}

{selectedTool.description && (

{selectedTool.description}

)}

Server: {selectedServer?.name}

{executionHistory.filter( (h) => h.toolName === selectedTool.name ).length > 0 && (

Runs:{' '} { executionHistory.filter( (h) => h.toolName === selectedTool.name ).length }

)}
{/* Tool Input Form */} {/* Tool Result */} {toolResult && ( )} {/* Execution History */}
)}
); }