Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
ToolPlayground.tsx33.9 kB
"use client"; import { useConfig } from "@/src/app/config-context"; import { HelpTooltip } from '@/src/components/utils/HelpTooltip'; import { executeFinalTransform, executeSingleStep, executeToolStepByStep, generateUUID, type StepExecutionResult } from "@/src/lib/client-utils"; import { formatBytes, generateUniqueKey, MAX_TOTAL_FILE_SIZE, sanitizeFileName, type UploadedFileInfo } from '@/src/lib/file-utils'; import { computeStepOutput } from "@/src/lib/utils"; import { ExecutionStep, Integration, SuperglueClient, Workflow as Tool, WorkflowResult as ToolResult } from "@superglue/client"; import { Loader2, X } from "lucide-react"; import { useRouter } from 'next/navigation'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { useToast } from "../../hooks/use-toast"; import { Button } from "../ui/button"; import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { ToolStepGallery } from "./ToolStepGallery"; export interface ToolPlaygroundProps { id?: string; embedded?: boolean; initialTool?: Tool; initialPayload?: string; initialInstruction?: string; integrations?: Integration[]; onSave?: (tool: Tool) => Promise<void>; onExecute?: (tool: Tool, result: ToolResult) => void; onInstructionEdit?: () => void; headerActions?: React.ReactNode; hideHeader?: boolean; readOnly?: boolean; selfHealingEnabled?: boolean; onSelfHealingChange?: (enabled: boolean) => void; shouldStopExecution?: boolean; onStopExecution?: () => void; uploadedFiles?: UploadedFileInfo[]; onFilesUpload?: (files: File[]) => Promise<void>; onFileRemove?: (key: string) => void; isProcessingFiles?: boolean; totalFileSize?: number; filePayloads?: Record<string, any>; } export interface ToolPlaygroundHandle { executeTool: (opts?: { selfHealing?: boolean }) => Promise<void>; saveTool: () => Promise<void>; getCurrentTool: () => Tool; } const ToolPlayground = forwardRef<ToolPlaygroundHandle, ToolPlaygroundProps>(({ id, embedded = false, initialTool, initialPayload, initialInstruction, integrations: providedIntegrations, onSave, onExecute, onInstructionEdit, headerActions, hideHeader = false, readOnly = false, selfHealingEnabled: externalSelfHealingEnabled, onSelfHealingChange, shouldStopExecution: externalShouldStop, onStopExecution, uploadedFiles: parentUploadedFiles, onFilesUpload: parentOnFilesUpload, onFileRemove: parentOnFileRemove, isProcessingFiles: parentIsProcessingFiles, totalFileSize: parentTotalFileSize, filePayloads: parentFilePayloads }, ref) => { const router = useRouter(); const { toast } = useToast(); const config = useConfig(); const [toolId, setToolId] = useState(initialTool?.id || ""); const [steps, setSteps] = useState<any[]>(initialTool?.steps || []); const [finalTransform, setFinalTransform] = useState(initialTool?.finalTransform || `(sourceData) => { return { result: sourceData } }`); const [responseSchema, setResponseSchema] = useState<string>( initialTool?.responseSchema ? JSON.stringify(initialTool.responseSchema, null, 2) : '' ); const [inputSchema, setInputSchema] = useState<string | null>( initialTool?.inputSchema ? JSON.stringify(initialTool.inputSchema, null, 2) : null ); const [payload, setPayload] = useState<string>(initialPayload || '{}'); // File upload state - use parent's if provided (embedded), otherwise use local const [localUploadedFiles, setLocalUploadedFiles] = useState<UploadedFileInfo[]>([]); const [localTotalFileSize, setLocalTotalFileSize] = useState(0); const [localIsProcessingFiles, setLocalIsProcessingFiles] = useState(false); const [localFilePayloads, setLocalFilePayloads] = useState<Record<string, any>>({}); // Use parent state if available, otherwise use local state const uploadedFiles = parentUploadedFiles || localUploadedFiles; const totalFileSize = parentTotalFileSize ?? localTotalFileSize; const isProcessingFiles = parentIsProcessingFiles ?? localIsProcessingFiles; const filePayloads = parentFilePayloads || localFilePayloads; useEffect(() => { if (initialPayload !== undefined) { setPayload(initialPayload); } }, [initialPayload]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [result, setResult] = useState<ToolResult | null>(null); const [error, setError] = useState<string | null>(null); const [completedSteps, setCompletedSteps] = useState<string[]>([]); const [failedSteps, setFailedSteps] = useState<string[]>([]); const [navigateToFinalSignal, setNavigateToFinalSignal] = useState<number>(0); const [showStepOutputSignal, setShowStepOutputSignal] = useState<number>(0); const [focusStepId, setFocusStepId] = useState<string | null>(null); const [stepResultsMap, setStepResultsMap] = useState<Record<string, any>>({}); const [isExecutingTransform, setIsExecutingTransform] = useState<boolean>(false); const [finalPreviewResult, setFinalPreviewResult] = useState<any>(null); // Track last user-edited step and previous step hashes to drive robust cascades const lastUserEditedStepIdRef = useRef<string | null>(null); const prevStepHashesRef = useRef<string[]>([]); const [integrations, setIntegrations] = useState<Integration[]>(providedIntegrations || []); const [instructions, setInstructions] = useState<string>(initialInstruction || ''); useEffect(() => { if (embedded && initialInstruction !== undefined) { setInstructions(initialInstruction); } }, [embedded, initialInstruction]); const [selfHealingEnabled, setSelfHealingEnabled] = useState(externalSelfHealingEnabled ?? true); const [isExecutingStep, setIsExecutingStep] = useState<number | undefined>(undefined); const [currentExecutingStepIndex, setCurrentExecutingStepIndex] = useState<number | undefined>(undefined); const [isStopping, setIsStopping] = useState(false); // Single source of truth for stopping across modes (embedded/standalone) const stopSignalRef = useRef<boolean>(false); useEffect(() => { if (externalSelfHealingEnabled !== undefined) { setSelfHealingEnabled(externalSelfHealingEnabled); } }, [externalSelfHealingEnabled]); const handleSelfHealingChange = (enabled: boolean) => { setSelfHealingEnabled(enabled); if (onSelfHealingChange) { onSelfHealingChange(enabled); } }; // Track latest external stop signal (embedded mode) in the single ref useEffect(() => { if (embedded) { stopSignalRef.current = !!externalShouldStop; } }, [externalShouldStop]); const handleStopExecution = () => { if (embedded && onStopExecution) { // Set stop signal immediately in embedded mode too stopSignalRef.current = true; onStopExecution(); } else { stopSignalRef.current = true; setIsStopping(true); toast({ title: "Stopping tool", description: "Tool will stop after the current step completes", }); } }; useImperativeHandle(ref, () => ({ executeTool, saveTool, getCurrentTool: () => ({ id: toolId, steps: steps.map((step: ExecutionStep) => ({ ...step, apiConfig: { id: step.apiConfig.id || step.id, ...step.apiConfig, pagination: step.apiConfig.pagination || null } })), responseSchema: responseSchema && responseSchema.trim() ? JSON.parse(responseSchema) : null, inputSchema: inputSchema ? JSON.parse(inputSchema) : null, finalTransform, instruction: instructions }) }), [toolId, steps, responseSchema, inputSchema, finalTransform, instructions]); const client = useMemo(() => new SuperglueClient({ endpoint: config.superglueEndpoint, apiKey: config.superglueApiKey, }), [config.superglueEndpoint, config.superglueApiKey]); // Unified file upload handlers const handleFilesUpload = async (files: File[]) => { // Use parent handler if available, otherwise handle locally if (parentOnFilesUpload) { return parentOnFilesUpload(files); } // Local handling for non-embedded mode setLocalIsProcessingFiles(true); try { const newSize = files.reduce((sum, f) => sum + f.size, 0); if (localTotalFileSize + newSize > MAX_TOTAL_FILE_SIZE) { toast({ title: 'Size limit exceeded', description: `Total file size cannot exceed ${formatBytes(MAX_TOTAL_FILE_SIZE)}`, variant: 'destructive' }); return; } const existingKeys = localUploadedFiles.map(f => f.key); const newFiles: UploadedFileInfo[] = []; for (const file of files) { try { const baseKey = sanitizeFileName(file.name); const key = generateUniqueKey(baseKey, [...existingKeys, ...newFiles.map(f => f.key)]); const fileInfo: UploadedFileInfo = { name: file.name, size: file.size, key, status: 'processing' }; newFiles.push(fileInfo); setLocalUploadedFiles(prev => [...prev, fileInfo]); const extractResult = await client.extract({ file: file }); if (!extractResult.success) { throw new Error(extractResult.error || 'Failed to extract data'); } const parsedData = extractResult.data; setLocalFilePayloads(prev => ({ ...prev, [key]: parsedData })); existingKeys.push(key); setLocalUploadedFiles(prev => prev.map(f => f.key === key ? { ...f, status: 'ready' } : f )); } catch (error: any) { const fileInfo = newFiles.find(f => f.name === file.name); if (fileInfo) { setLocalUploadedFiles(prev => prev.map(f => f.key === fileInfo.key ? { ...f, status: 'error', error: error.message } : f )); } toast({ title: 'File processing failed', description: `Failed to parse ${file.name}: ${error.message}`, variant: 'destructive' }); } } setLocalTotalFileSize(prev => prev + newSize); } finally { setLocalIsProcessingFiles(false); } }; const handleFileRemove = (key: string) => { // Use parent handler if available if (parentOnFileRemove) { return parentOnFileRemove(key); } // Local handling const fileToRemove = localUploadedFiles.find(f => f.key === key); if (!fileToRemove) return; setLocalFilePayloads(prev => { const newPayloads = { ...prev }; delete newPayloads[key]; return newPayloads; }); setLocalUploadedFiles(prev => prev.filter(f => f.key !== key)); setLocalTotalFileSize(prev => Math.max(0, prev - fileToRemove.size)); }; const loadIntegrations = async () => { if (providedIntegrations) return; try { setLoading(true); const result = await client.listIntegrations(100, 0); setIntegrations(result.items); return result.items; } catch (error: any) { console.error("Error loading integrations:", error); toast({ title: "Error loading integrations", description: error.message, variant: "destructive", }); } finally { setLoading(false); } }; const loadTool = async (idToLoad: string) => { try { if (!idToLoad) return; setLoading(true); setResult(null); const tool = await client.getWorkflow(idToLoad); if (!tool) { throw new Error(`Tool with ID "${idToLoad}" not found.`); } setToolId(tool.id || ''); setSteps(tool?.steps?.map(step => ({ ...step, apiConfig: { ...step.apiConfig, id: step.apiConfig.id || step.id } })) || []); setFinalTransform(tool.finalTransform || `(sourceData) => { return { result: sourceData } }`); setInstructions(tool.instruction || ''); setResponseSchema(tool.responseSchema ? JSON.stringify(tool.responseSchema, null, 2) : ''); setInputSchema(tool.inputSchema ? JSON.stringify(tool.inputSchema, null, 2) : null); // Don't modify payload when loading a tool - keep existing or use empty object } catch (error: any) { console.error("Error loading tool:", error); toast({ title: "Error loading tool", description: error.message, variant: "destructive", }); } finally { setLoading(false); } }; useEffect(() => { if (!embedded && !providedIntegrations) { loadIntegrations(); } }, [embedded, providedIntegrations]); useEffect(() => { if (providedIntegrations) { setIntegrations(providedIntegrations); } }, [providedIntegrations]); const [lastToolId, setLastToolId] = useState<string | undefined>(initialTool?.id); useEffect(() => { if (initialTool && initialTool.id !== lastToolId) { setToolId(initialTool.id || ''); setSteps(initialTool.steps?.map(step => ({ ...step, apiConfig: { ...step.apiConfig, id: step.apiConfig.id || step.id } })) || []); setFinalTransform(initialTool.finalTransform || `(sourceData) => { return { result: sourceData } }`); const schemaString = initialTool.responseSchema ? JSON.stringify(initialTool.responseSchema, null, 2) : ''; setResponseSchema(schemaString); setInputSchema(initialTool.inputSchema ? JSON.stringify(initialTool.inputSchema, null, 2) : null); setInstructions(initialInstruction || initialTool.instruction || ''); setLastToolId(initialTool.id); } }, [initialTool, embedded, lastToolId, initialInstruction]); useEffect(() => { if (!embedded && id) { loadTool(id); } else if (!embedded && !id && !initialTool) { setToolId(""); setSteps([]); setInstructions(""); setFinalTransform(`(sourceData) => { return { result: sourceData } }`); setResponseSchema(''); setInputSchema(null); setPayload('{}'); setResult(null); setFinalPreviewResult(null); } }, [id, embedded, initialTool]); const saveTool = async () => { try { try { JSON.parse(responseSchema || '{}'); } catch (e) { throw new Error("Invalid response schema JSON"); } try { JSON.parse(inputSchema || '{}'); } catch (e) { throw new Error("Invalid input schema JSON"); } if (!toolId.trim()) { setToolId(`wf-${Date.now()}`); } setSaving(true); // Always use the current steps (which include any self-healed updates) const stepsToSave = steps; const toolToSave: Tool = { id: toolId, // Save the self-healed steps if they exist (from a successful run with self-healing enabled) steps: stepsToSave.map((step: ExecutionStep) => ({ ...step, apiConfig: { id: step.apiConfig.id || step.id, ...step.apiConfig, pagination: step.apiConfig.pagination || null } })), // Only save responseSchema if it's explicitly enabled (non-empty string) responseSchema: responseSchema && responseSchema.trim() ? JSON.parse(responseSchema) : null, inputSchema: inputSchema ? JSON.parse(inputSchema) : null, finalTransform, instruction: instructions } as any; // In embedded mode, use the provided onSave callback if (embedded && onSave) { await onSave(toolToSave); } else { // In standalone mode, save to backend const savedTool = await client.upsertWorkflow(toolId, toolToSave as any); if (!savedTool) { throw new Error("Failed to save tool"); } setToolId(savedTool.id); toast({ title: "Tool saved", description: `"${savedTool.id}" saved successfully`, }); } } catch (error: any) { console.error("Error saving tool:", error); toast({ title: "Error saving tool", description: error.message, variant: "destructive", }); } finally { setSaving(false); } }; const executeTool = async (opts?: { selfHealing?: boolean }) => { setLoading(true); // Fully clear any stale stop signals from a previous run (both modes) stopSignalRef.current = false; setIsStopping(false); setCompletedSteps([]); setFailedSteps([]); setResult(null); setFinalPreviewResult(null); setStepResultsMap({}); setError(null); setFocusStepId(null); try { JSON.parse(responseSchema || '{}'); JSON.parse(inputSchema || '{}'); // Always use the current steps for execution const executionSteps = steps; const currentResponseSchema = responseSchema && responseSchema.trim() ? JSON.parse(responseSchema) : null; const effectiveSelfHealing = opts?.selfHealing ?? selfHealingEnabled; const tool = { id: toolId, steps: executionSteps, finalTransform, responseSchema: currentResponseSchema, inputSchema: inputSchema ? JSON.parse(inputSchema) : null, } as any; // Store original steps to compare against self-healed result const originalStepsJson = JSON.stringify(executionSteps); // Merge manual payload with file payloads for execution const manualPayload = JSON.parse(payload || '{}'); const payloadObj = { ...manualPayload, ...filePayloads }; setCurrentExecutingStepIndex(0); const state = await executeToolStepByStep( client, tool, payloadObj, (i: number, res: StepExecutionResult) => { if (i < tool.steps.length - 1) { setCurrentExecutingStepIndex(i + 1); } else { setCurrentExecutingStepIndex(tool.steps.length); } if (res.success) { setCompletedSteps(prev => Array.from(new Set([...prev, res.stepId]))); } else { setFailedSteps(prev => Array.from(new Set([...prev, res.stepId]))); } try { const normalized = computeStepOutput(res); setStepResultsMap(prev => ({ ...prev, [res.stepId]: normalized.output })); } catch { } }, effectiveSelfHealing, () => stopSignalRef.current ); if (state.interrupted) { toast({ title: "Tool interrupted", description: `Stopped at step ${Math.min(state.currentStepIndex + 1, tool.steps.length)} (${tool.steps[state.currentStepIndex]?.id || 'n/a'})`, }); } // Always update steps with returned configuration (API may normalize/update even without self-healing) if (state.currentTool.steps) { const returnedStepsJson = JSON.stringify(state.currentTool.steps); if (originalStepsJson !== returnedStepsJson) { setSteps(state.currentTool.steps); // Only show toast if self-healing was enabled (otherwise it's likely just normalization) if (effectiveSelfHealing) { toast({ title: "Tool configuration updated", description: "Self-healing has modified the tool configuration to fix issues.", }); } } } const stepDataMap: Record<string, any> = {}; Object.entries(state.stepResults).forEach(([stepId, res]) => { const normalized = computeStepOutput(res as StepExecutionResult); stepDataMap[stepId] = normalized.output; }); setStepResultsMap(stepDataMap); const finalData = state.stepResults['__final_transform__']?.data; setFinalPreviewResult(finalData); const wr: ToolResult = { id: generateUUID(), success: state.failedSteps.length === 0, data: finalData, error: state.stepResults['__final_transform__']?.error, startedAt: new Date(), completedAt: new Date(), stepResults: Object.entries(state.stepResults) .filter(([key]) => key !== '__final_transform__') .map(([stepId, result]: [string, StepExecutionResult]) => ({ stepId, success: result.success || !result.error, data: result.data || result, error: result.error })), config: { id: toolId, steps: state.currentTool.steps, finalTransform: state.currentTool.finalTransform || finalTransform, } as any }; setResult(wr); // Update finalTransform with the self-healed version if it was modified if (state.currentTool.finalTransform && effectiveSelfHealing) { setFinalTransform(state.currentTool.finalTransform); } setCompletedSteps(state.completedSteps); setFailedSteps(state.failedSteps); if (state.failedSteps.length === 0 && !state.interrupted) { setNavigateToFinalSignal(Date.now()); } else { const firstFailed = state.failedSteps[0]; if (firstFailed) { setFocusStepId(firstFailed); setShowStepOutputSignal(Date.now()); const err = (state.stepResults[firstFailed] as any)?.error || 'Step execution failed'; toast({ title: "Step failed", description: `${firstFailed}: ${typeof err === 'string' ? err : 'Execution error'}`, variant: "destructive" }); } } if (onExecute) { const executedTool = { id: toolId, steps: executionSteps, finalTransform: state.currentTool.finalTransform || finalTransform, responseSchema: currentResponseSchema, inputSchema: inputSchema ? JSON.parse(inputSchema) : null, instruction: instructions } as Tool; onExecute(executedTool, wr); } } catch (error: any) { console.error("Error executing tool:", error); toast({ title: "Error executing tool", description: error.message, variant: "destructive", }); } finally { setLoading(false); setIsStopping(false); setCurrentExecutingStepIndex(undefined); // Ensure stop signal is reset after a run finishes/interrupted stopSignalRef.current = false; } }; const handleStepsChange = (newSteps: any[]) => { setSteps(newSteps); }; const handleStepEdit = (stepId: string, updatedStep: any, isUserInitiated: boolean = false) => { // No-op guard: avoid cascades if nothing actually changed const idx = steps.findIndex(s => s.id === stepId); if (idx !== -1) { const current = steps[idx]; const currHash = hashStepConfig(current); const nextHash = hashStepConfig(updatedStep); if (currHash === nextHash) return; } // Update the steps immediately setSteps(prevSteps => prevSteps.map(step => (step.id === stepId ? { ...updatedStep, apiConfig: { ...updatedStep.apiConfig, id: updatedStep.apiConfig.id || updatedStep.id } } : step)) ); // Mark which step was edited by the user (used by steps effect to cascade resets) if (isUserInitiated) { lastUserEditedStepIdRef.current = stepId; } }; // Compute a stable hash for a step's configuration that affects execution const hashStepConfig = (s: any): string => { try { const exec = { id: s.id, executionMode: s.executionMode, loopSelector: s.loopSelector, loopMaxIters: s.loopMaxIters, integrationId: s.integrationId, apiConfig: s.apiConfig, }; return JSON.stringify(exec); } catch { return ''; } }; // Drive cascading resets off of the source-of-truth: steps changes useEffect(() => { const currentHashes = steps.map(hashStepConfig); const prevHashes = prevStepHashesRef.current; // Only cascade when the edited step itself changed if (lastUserEditedStepIdRef.current) { const editedId = lastUserEditedStepIdRef.current; const idxOfEdited = steps.findIndex(s => s.id === editedId); if (idxOfEdited !== -1 && prevHashes[idxOfEdited] !== currentHashes[idxOfEdited]) { const stepsToReset = steps.slice(idxOfEdited).map(s => s.id); setCompletedSteps(prev => prev.filter(id => !stepsToReset.includes(id) && id !== '__final_transform__')); setFailedSteps(prev => prev.filter(id => !stepsToReset.includes(id) && id !== '__final_transform__')); setStepResultsMap(prev => { const next = { ...prev } as Record<string, any>; stepsToReset.forEach(id => delete next[id]); delete next['__final_transform__']; return next; }); setFinalPreviewResult(null); setResult(null); } // Clear marker regardless to avoid stale cascades lastUserEditedStepIdRef.current = null; } // Update previous hashes after processing prevStepHashesRef.current = currentHashes; }, [steps]); const handleExecuteStep = async (idx: number) => { try { // mark testing state for indicator without freezing entire UI setIsExecutingStep(idx); const single = await executeSingleStep( client, { id: toolId, steps } as any, idx, JSON.parse(payload || '{}'), stepResultsMap, // Pass accumulated results false ); const sid = steps[idx].id; const normalized = computeStepOutput(single); const isFailure = !single.success; // Update step configuration if API returned changes if (single.updatedStep) { setSteps(prevSteps => prevSteps.map((step, i) => i === idx ? single.updatedStep : step) ); } if (isFailure) { setFailedSteps(prev => Array.from(new Set([...prev.filter(id => id !== sid), sid]))); setCompletedSteps(prev => prev.filter(id => id !== sid)); } else { setCompletedSteps(prev => Array.from(new Set([...prev.filter(id => id !== sid), sid]))); setFailedSteps(prev => prev.filter(id => id !== sid)); } setStepResultsMap(prev => ({ ...prev, [sid]: normalized.output })); setFocusStepId(sid); setShowStepOutputSignal(Date.now()); if (isFailure) { toast({ title: "Step failed", description: `${sid}: ${single.error || 'Execution error'}`, variant: "destructive" }); } } finally { setIsExecutingStep(undefined); } }; const handleExecuteTransform = async (schemaStr: string, transformStr: string) => { try { setIsExecutingTransform(true); // Build the payload with all step results const stepData: Record<string, any> = {}; Object.entries(stepResultsMap).forEach(([stepId, result]) => { if (stepId !== '__final_transform__') { stepData[stepId] = result?.data !== undefined ? result.data : result; } }); const parsedResponseSchema = schemaStr && schemaStr.trim() ? JSON.parse(schemaStr) : null; const manualPayload = JSON.parse(payload || '{}'); const fullPayload = { ...manualPayload, ...filePayloads }; const result = await executeFinalTransform( client, toolId || 'test', transformStr || finalTransform, parsedResponseSchema, inputSchema ? JSON.parse(inputSchema) : null, fullPayload, stepData, false ); if (result.success) { setCompletedSteps(prev => Array.from(new Set([...prev.filter(id => id !== '__final_transform__'), '__final_transform__']))); setFailedSteps(prev => prev.filter(id => id !== '__final_transform__')); setStepResultsMap(prev => ({ ...prev, ['__final_transform__']: result.data })); setFinalPreviewResult(result.data); setNavigateToFinalSignal(Date.now()); toast({ title: "Transform executed successfully", description: "Final transform completed", }); } else { setFailedSteps(prev => Array.from(new Set([...prev.filter(id => id !== '__final_transform__'), '__final_transform__']))); setCompletedSteps(prev => prev.filter(id => id !== '__final_transform__')); // Store error message for display setStepResultsMap(prev => ({ ...prev, ['__final_transform__']: result.error || 'Transform execution failed' })); toast({ title: "Transform execution failed", description: result.error || "Failed to execute final transform", variant: "destructive", }); } } finally { setIsExecutingTransform(false); } }; // Default header actions for standalone mode const defaultHeaderActions = ( <div className="flex items-center gap-2"> <div className="flex items-center gap-2 mr-2"> <Label htmlFor="selfHealing-top" className="text-xs flex items-center gap-1"> <span>Self-healing</span> </Label> <div className="flex items-center"> <Switch className="custom-switch" id="selfHealing-top" checked={selfHealingEnabled} onCheckedChange={handleSelfHealingChange} /> <div className="ml-1 flex items-center"> <HelpTooltip text="Enable self-healing during execution. Slower, but can auto-fix failures in tool steps and transformation code." /> </div> </div> </div> {loading ? ( <Button variant="destructive" onClick={handleStopExecution} disabled={saving || (isExecutingStep !== undefined) || isExecutingTransform || isStopping} className="h-9 px-4" > {isStopping ? "Stopping..." : "Stop Execution"} </Button> ) : ( <Button variant="success" onClick={() => executeTool()} disabled={loading || saving || (isExecutingStep !== undefined) || isExecutingTransform} className="h-9 px-4" > Test Tool </Button> )} <Button variant="default" onClick={saveTool} disabled={saving || loading} className="h-9 px-5 shadow-md border border-primary/40" > {saving ? "Saving Tool..." : "Save Tool"} </Button> </div> ); return ( <div className={embedded ? "w-full" : "p-6 max-w-none w-full"} style={{ scrollbarGutter: 'stable both-edges' }}> {!embedded && !hideHeader && ( <> <div className="flex justify-end items-center mb-2"> <Button variant="ghost" size="icon" className="shrink-0" onClick={() => router.push('/configs')} aria-label="Close" > <X className="h-4 w-4" /> </Button> </div> <h1 className="text-2xl font-bold mb-3 flex-shrink-0">Run and Edit Tool</h1> </> )} <div className="w-full overflow-y-auto pr-4" style={{ maxHeight: 'calc(100vh - 140px)' }}> <div className="w-full"> <div className="space-y-4"> <div className={embedded ? "" : "mb-4"}> {loading && steps.length === 0 && !instructions ? ( <div className="flex items-center justify-center py-20"> <div className="flex flex-col items-center gap-3"> <Loader2 className="h-8 w-8 animate-spin text-foreground" /> </div> </div> ) : ( <ToolStepGallery steps={steps} stepResults={stepResultsMap} finalTransform={finalTransform} finalResult={result?.data} transformResult={finalPreviewResult} responseSchema={responseSchema} toolId={toolId} instruction={instructions} onStepsChange={handleStepsChange} onStepEdit={handleStepEdit} onExecuteStep={handleExecuteStep} onExecuteTransform={handleExecuteTransform} onFinalTransformChange={setFinalTransform} onResponseSchemaChange={setResponseSchema} onPayloadChange={setPayload} onToolIdChange={setToolId} onInstructionEdit={embedded ? onInstructionEdit : undefined} integrations={integrations} isExecuting={loading} isExecutingStep={isExecutingStep} isExecutingTransform={isExecutingTransform as any} currentExecutingStepIndex={currentExecutingStepIndex} completedSteps={completedSteps} failedSteps={failedSteps} readOnly={readOnly} inputSchema={inputSchema} onInputSchemaChange={(v) => setInputSchema(v)} payloadText={payload} headerActions={headerActions || (!embedded ? defaultHeaderActions : undefined)} navigateToFinalSignal={navigateToFinalSignal} showStepOutputSignal={showStepOutputSignal} focusStepId={focusStepId} uploadedFiles={uploadedFiles} onFilesUpload={handleFilesUpload} onFileRemove={handleFileRemove} isProcessingFiles={isProcessingFiles} totalFileSize={totalFileSize} filePayloads={filePayloads} /> )} </div> </div> </div> </div> </div> ); }); ToolPlayground.displayName = 'ToolPlayground'; export default ToolPlayground;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/superglue-ai/superglue'

If you have feedback or need assistance with the MCP directory API, please join our Discord server