Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
ExtractCreateStepper.tsx37 kB
'use client' import { useConfig } from '@/src/app/config-context'; import { useToast } from '@/src/hooks/use-toast'; import { findArraysOfObjects, parseCredentialsHelper } from '@/src/lib/client-utils'; import { cn, inputErrorStyles } from '@/src/lib/utils'; import { ApiConfig, AuthType, CacheMode, ExtractConfig, SuperglueClient, TransformConfig } from '@superglue/client'; import { Copy, Download, Loader2, Terminal, Upload } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { Button } from '../ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Textarea } from '../ui/textarea'; import { HelpTooltip } from '../utils/HelpTooltip'; import { API_CREATE_STEPS, StepIndicator, type StepperStep } from '../utils/StepIndicator'; import { InteractiveExtractPlayground } from './InteractiveExtractPlayground'; interface ExtractCreateStepperProps { open: boolean onOpenChange: (open: boolean) => void extractId?: string // not used, but if we later want to edit through this flow mode?: 'create' | 'edit' } export function ExtractCreateStepper({ open, onOpenChange, extractId: initialExtractId, mode = 'create' }: ExtractCreateStepperProps) { const [step, setStep] = useState<StepperStep>('basic') const [isAutofilling, setIsAutofilling] = useState(false) const { toast } = useToast() const router = useRouter() const superglueConfig = useConfig() const [extractId, setExtractId] = useState<string>(initialExtractId || '') const [file, setFile] = useState<File | null>(null) const [initialRawResponse, setInitialRawResponse] = useState<any>(null) const [hasMappedResponse, setHasMappedResponse] = useState(false) const [mappedResponseData, setMappedResponseData] = useState<any>(null) const [responseMapping, setResponseMapping] = useState<any>(null) const [isRunning, setIsRunning] = useState(false) const [formData, setFormData] = useState({ urlHost: '', urlPath: '', instruction: '', documentationUrl: '', inputPayload: '{}', auth: { type: AuthType.HEADER, value: '', advancedConfig: '{}' }, responseSchema: '{}' }) const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({}) // Add new state for drag and drop const [isDragging, setIsDragging] = useState(false) const [activeSourceTab, setActiveSourceTab] = useState<'upload' | 'url'>('upload') const handleChange = (field: string) => ( e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string ) => { const value = typeof e === 'string' ? e : e.target.value setFormData(prev => ({ ...prev, [field]: value })) // Reset hasMappedResponse and mappedResponseData when schema or instruction changes if (field === 'responseSchema' || field === 'instruction') { setHasMappedResponse(false) setMappedResponseData(null) } } const handleAuthChange = (value: string) => { setFormData(prev => ({ ...prev, auth: { ...prev.auth, value } })) } const handleNext = async () => { if (step === 'basic') { const errors: Record<string, boolean> = {} if (activeSourceTab === 'url' && !formData.urlHost) { errors.urlHost = true } if (activeSourceTab === 'upload' && !file) { errors.file = true } if (!formData.instruction) { errors.instruction = true } if (Object.keys(errors).length > 0) { setValidationErrors(errors) // Find first error field and scroll to it const firstErrorField = Object.keys(errors)[0] const errorElement = document.getElementById(firstErrorField) if (errorElement) { errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) errorElement.focus() } return } setValidationErrors({}) if (file) { try { setIsAutofilling(true) await fetchFromFile() setIsAutofilling(false) setStep('try_and_output') return } catch (error: any) { setIsAutofilling(false) toast({ title: 'Extraction Failed', description: error?.message || 'An error occurred while configuring the Extraction', variant: 'destructive' }) return } } // If URL is selected, fetch from config if (activeSourceTab === 'url') { setIsAutofilling(true) try { await fetchFromConfig() } catch (error: any) { console.error('Error during autofill:', error) toast({ title: 'Autofill Failed', description: error?.message || 'An error occurred while configuring the API', variant: 'destructive' }) return } finally { setIsAutofilling(false) } } } if (step === 'try_and_output') { // Save the configuration with the updated schema try { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }) const savedConfig = await superglueClient.upsertExtraction(extractId, { id: extractId, urlHost: formData.urlHost, instruction: formData.instruction, documentationUrl: formData.documentationUrl || undefined, } as ExtractConfig) const savedTransformation = await superglueClient.upsertTransformation(extractId, { id: extractId, responseSchema: JSON.parse(formData.responseSchema), } as TransformConfig) if (!savedConfig || !savedTransformation) { throw new Error('Failed to save configuration') } } catch (error: any) { console.error('Error saving config:', error) toast({ title: 'Error Saving Configuration', description: error?.message || 'An error occurred while saving the configuration', variant: 'destructive' }) return } } const steps: StepperStep[] = ['basic', 'try_and_output', 'success'] const currentIndex = steps.indexOf(step) if (currentIndex < steps.length - 1) { setStep(steps[currentIndex + 1]) } } const handleBack = () => { const steps: StepperStep[] = ['basic', 'try_and_output', 'success'] const currentIndex = steps.indexOf(step) if (currentIndex > 0) { setStep(steps[currentIndex - 1]) } } const handleClose = () => { if (mode === 'create') { router.push('/configs') } else { router.push(`/configs/${extractId}/edit`) } onOpenChange(false) } const getCurlCommand = () => { if (file) { const extractCommand = `# First command: Extract data and store in JSON curl -s -X POST "${superglueConfig.superglueEndpoint}" \\ -H "Authorization: Bearer ${superglueConfig.superglueApiKey}" \\ -F 'operations={"query":"mutation($file: Upload!) { extract(input: { file: $file }, options: { timeout: 30000 }) { id success data error } }", "variables": { "file": null }}' \\ -F 'map={"0": ["variables.file"]}' \\ -F '0=@${file.name}' | \\ jq '.data.extract.data' > extracted_data.json` const transformCommand = `# Second command: Transform the extracted data curl -s -X POST "${superglueConfig.superglueEndpoint}" \\ -H "Authorization: Bearer ${superglueConfig.superglueApiKey}" \\ -H "Content-Type: application/json" \\ -d @<(jq -n --argjson data "$(cat extracted_data.json)" '{ "query": "mutation($data: JSON!) { transform(input: { id: \\"${extractId}\\" }, data: $data) { data } }", "variables": { "data": $data } }')` return `${extractCommand}\n\n${transformCommand}` } // Regular API endpoint curl commands let payload = {} try { payload = JSON.parse(formData.inputPayload) } catch (e) { console.warn('Invalid input payload JSON') } const credentials = parseCredentialsHelper(formData.auth.value) const extractCommand = `# First command: Extract data and store in JSON curl -s -X POST "${superglueConfig.superglueEndpoint}" \\ -H "Authorization: Bearer ${superglueConfig.superglueApiKey}" \\ -H "Content-Type: application/json" \\ -d '{ "query": "mutation($payload: JSON, $credentials: JSON) { extract(input: { id: \\"${extractId}\\" }, payload: $payload, credentials: $credentials) { data } }", "variables": { "payload": ${JSON.stringify(payload, null, 2)}, "credentials": ${JSON.stringify(credentials, null, 2)} } }' | jq '.data.extract.data' > extracted_data.json` const transformCommand = `# Second command: Transform the extracted data curl -s -X POST "${superglueConfig.superglueEndpoint}" \\ -H "Authorization: Bearer ${superglueConfig.superglueApiKey}" \\ -H "Content-Type: application/json" \\ -d @<(jq -n --argjson data "$(cat extracted_data.json)" '{ "query": "mutation($data: JSON!) { transform(input: { id: \\"${extractId}\\" }, data: $data) { data } }", "variables": { "data": $data } }')` return `${extractCommand}\n\n${transformCommand}` } const getSdkCode = () => { const credentials = parseCredentialsHelper(formData.auth.value) return `npm install @superglue/client // in your app: import { SuperglueClient } from "@superglue/client"; const superglue = new SuperglueClient({ apiKey: "${superglueConfig.superglueApiKey}" }); const extractResult = await superglue.extract({ id: "${extractId}", payload: ${formData.inputPayload}, credentials: ${JSON.stringify(credentials)} }); const transformResult = await superglue.transform({ id: "${extractId}", data: extractResult.data }); if (transformResult?.success) { console.log('Transformed Data:', transformResult.data); } else { console.error('Transformation Error:', transformResult.error); } ` } const fetchFromConfig = async () => { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }) // Call autofill endpoint const response = await superglueClient.extract({ endpoint: { id: formData.urlHost?.replace(/^(https?|postgres(ql)?|ftp(s)?|sftp|file):\/\//, '').replace(/\//g, '') + '-' + Math.floor(1000 + Math.random() * 9000), urlHost: formData.urlHost, ...(formData.urlPath ? { urlPath: formData.urlPath } : {}), ...(formData.documentationUrl ? { documentationUrl: formData.documentationUrl } : {}), instruction: formData.instruction, authentication: formData.auth.value ? AuthType.HEADER : AuthType.NONE }, payload: JSON.parse(formData.inputPayload), credentials: parseCredentialsHelper(formData.auth.value), options: { cacheMode: CacheMode.DISABLED } }) if (response.error) { throw new Error(response.error) } // Store the raw response for the try step setInitialRawResponse(response.data) // Generate schema based on the raw response const generatedSchema = await superglueClient.generateSchema(formData.instruction, JSON.stringify(response.data)) if (generatedSchema) { setFormData(prev => ({ ...prev, responseSchema: JSON.stringify(generatedSchema, null, 2) })) } // Apply the returned config const config = response.config as ApiConfig if (config) { const id = formData.urlHost.replace(/^(https?|postgres(ql)?|ftp(s)?|sftp|file):\/\//, '').replace(/\//g, '') + '-' + Math.floor(1000 + Math.random() * 9000) setExtractId(id) // Save the configuration with the generated schema const savedConfig = await superglueClient.upsertApi(id, { id, ...config, responseSchema: generatedSchema, createdAt: new Date(), updatedAt: new Date() } as ApiConfig) if (!savedConfig) { throw new Error('Failed to save configuration') } } } const fetchFromFile = async () => { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }) const response = await superglueClient.extract({ file: file }) if (response.error) { throw new Error(response.error) } setInitialRawResponse(response.data) setExtractId(response.config.id) const generatedSchema = await superglueClient.generateSchema(formData.instruction, JSON.stringify(response.data)) if (generatedSchema) { setFormData(prev => ({ ...prev, responseSchema: JSON.stringify(generatedSchema, null, 2) })) } } // Update handleMappedResponse to store the response data const handleMappedResponse = (response: any) => { setMappedResponseData(response) setHasMappedResponse(!!response && typeof response === 'object') } const handleRun = async () => { // TODO: dedupe this InteractiveApiPlayground setIsRunning(true) try { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }) // 1. First upsert the API config with the new schema and instruction await superglueClient.upsertExtraction(extractId, { id: extractId, instruction: formData.instruction, }) let mappedData = initialRawResponse; let responseMapping = null; if (formData.responseSchema && Object.keys(JSON.parse(formData.responseSchema)).length > 0) { await superglueClient.upsertTransformation(extractId, { responseSchema: JSON.parse(formData.responseSchema), instruction: formData.instruction }); const mappedResult = await superglueClient.transform({ id: extractId, data: initialRawResponse }); if (mappedResult.error) { throw new Error(mappedResult.error); } mappedData = mappedResult.data; responseMapping = (mappedResult.config as TransformConfig).responseMapping; } setInitialRawResponse(initialRawResponse); setMappedResponseData(mappedData); setResponseMapping(responseMapping); setHasMappedResponse(true); } catch (error: any) { console.error('Error running API:', error) toast({ title: 'Error Running API', description: error?.message || 'An error occurred while running the API', variant: 'destructive' }) } finally { setIsRunning(false) } } // Add drag and drop handlers const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) } const handleDrop = async (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const file = e.dataTransfer.files[0] if (!file) return setFile(file) setValidationErrors({}) } // Update handleFileUpload to use processFile const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0] if (!file) return setFile(file) setValidationErrors({}) } // Add this helper function near the top with other functions const downloadData = (data: Record<string, any>, format: 'json' | 'csv') => { if (format === 'json') { const content = JSON.stringify(data, null, 2); const blob = new Blob([content], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `superglue_extract_${extractId}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } else { const objects = findArraysOfObjects(data); Object.keys(objects).forEach(key => { const items = objects[key]; const replacer = (key: string, value: any) => value === null ? '' : value; const header = Object.keys(items[0] || {}); const csv = [ header.join(','), ...items.map(row => header.map(field => JSON.stringify(row[field], replacer)).join(',')) ].join('\r\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `superglue_extract_${extractId}_${key}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); }); } } return ( <Dialog open={open} onOpenChange={handleClose}> <DialogContent className="h-[100vh] w-[100vw] max-w-[100vw] p-3 sm:p-6 lg:p-12 gap-0 rounded-none border-none flex flex-col" onPointerDownOutside={e => e.preventDefault()} > <div className="flex-none mb-4"> <DialogHeader> <div className="flex flex-col lg:flex-row items-center justify-between gap-4 mb-4"> <DialogTitle> {step === 'success' ? 'Configuration Complete!' : 'Create New Document Configuration'} </DialogTitle> {!step.includes('success') && ( <Button variant="outline" className="bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-200/50 hover:border-blue-300/50 text-blue-600 hover:text-blue-700 text-sm px-4 py-1 h-8 rounded-full animate-pulse shrink-0" onClick={() => window.open('https://cal.com/superglue/onboarding', '_blank')} > ✨ Get help from our team </Button> )} </div> </DialogHeader> <StepIndicator currentStep={step} steps={API_CREATE_STEPS} /> </div> <div className="flex-1 overflow-y-auto px-1 min-h-0"> {step === 'basic' && ( <div className="space-y-3"> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="instruction">What do you want to get from this source?</Label> <HelpTooltip text="Describe what data you want to extract from this source in plain English" /> </div> <Textarea id="instruction" value={formData.instruction} autoFocus onChange={(e) => { handleChange('instruction')(e) if (e.target.value) { setValidationErrors(prev => ({ ...prev, instruction: false })) } }} placeholder="E.g. 'Get all products with price and name'" className={cn( "h-24", validationErrors.instruction && inputErrorStyles, validationErrors.instruction && "focus:!border-destructive" )} required /> {validationErrors.instruction && ( <p className="text-sm text-destructive mt-1">Instruction is required</p> )} </div> <Tabs defaultValue="upload" className="w-full" value={activeSourceTab} onValueChange={(value) => setActiveSourceTab(value as 'upload' | 'url')} > <TabsList className="grid w-full grid-cols-2"> <TabsTrigger value="upload">Upload File</TabsTrigger> <TabsTrigger value="url">Configure from URL</TabsTrigger> </TabsList> <TabsContent value="upload"> <div className={cn( "relative rounded-lg border-2 border-dashed p-8", isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25", file ? "border-blue-500/30 bg-blue-500/5 ring-1 ring-blue-500/20" : "", "transition-all duration-200 ease-in-out", validationErrors.file && "border-destructive" )} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} > <div className="flex flex-col items-center gap-3 text-center"> {file ? ( <> <div className="flex items-center gap-3 bg-blue-500/10 px-4 py-2 rounded-full"> <Upload className="h-5 w-5 text-blue-500" /> <span className="text-blue-700 font-medium">{file.name}</span> </div> <div className="flex items-center gap-2 text-sm text-muted-foreground"> <Button variant="ghost" size="sm" className="text-muted-foreground hover:text-destructive transition-colors" onClick={() => setFile(null)} > Remove </Button> </div> </> ) : ( <> <Upload className="h-8 w-8 text-muted-foreground/60" /> <div> <Button variant="ghost" className={cn( "text-primary font-medium hover:text-primary/80", validationErrors.file && "text-destructive" )} onClick={() => document.getElementById('file-upload')?.click()} > Upload File </Button> <input type="file" id="file-upload" className="hidden" onChange={handleFileUpload} /> <p className="text-sm text-muted-foreground mt-1">or drag and drop your file here</p> </div> </> )} </div> {isDragging && ( <div className="absolute inset-0 bg-primary/5 backdrop-blur-[1px] flex items-center justify-center rounded-lg border-2 border-primary transition-all duration-200"> <p className="text-sm font-medium text-primary">Drop file here</p> </div> )} </div> </TabsContent> <TabsContent value="url" className="space-y-3"> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="urlHost">API Endpoint Domain</Label> <HelpTooltip text="The base URL of the API (e.g., https://api.example.com)." /> </div> <Input id="urlHost" value={formData.urlHost} onChange={(e) => { handleChange('urlHost')(e) if (e.target.value) { setValidationErrors(prev => ({ ...prev, urlHost: false })) } }} placeholder="https://api.example.com" required className={cn( validationErrors.urlHost && inputErrorStyles, validationErrors.urlHost && "focus:!border-destructive" )} /> {validationErrors.urlHost && ( <p className="text-sm text-destructive mt-1">API endpoint is required</p> )} </div> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="urlPath">API Endpoint Path (Optional)</Label> <HelpTooltip text="Additional path after the domain (e.g., /v1/apis). If you leave this blank, superglue will figure out the path automatically." /> </div> <Input id="urlPath" value={formData.urlPath} onChange={handleChange('urlPath')} placeholder="/v1/apis" /> </div> <div> <div className="flex items-center gap-2 mb-1.5"> <Label htmlFor="auth">API Key or Token for Target API (Optional)</Label> <HelpTooltip text="Enter API key/token here, superglue figures out where to put it. Do not include prefixes like Bearer or Basic in the field." /> </div> <Input id="auth" value={formData.auth.value} onChange={(e) => handleAuthChange(e.target.value)} placeholder="Enter your API key or token" /> </div> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="documentationUrl">API Documentation (Optional)</Label> <HelpTooltip text="Link to the API's documentation if available" /> </div> <Input id="documentationUrl" value={formData.documentationUrl} onChange={handleChange('documentationUrl')} placeholder="https://docs.example.com" /> </div> <div> <div className="flex gap-2 items-stretch"> <div className="flex-1"> <Button variant="outline" onClick={() => setFormData(prev => ({ ...prev, inputPayload: prev.inputPayload === '{}' ? JSON.stringify({ query: "", filters: {} }, null, 2) : '{}' }))} className="w-full h-full" > {formData.inputPayload === '{}' ? 'Add Request Variables (Optional)' : 'Remove Request Variables'} </Button> </div> </div> {formData.inputPayload !== '{}' && ( <div className="mt-3"> <div className="flex items-center gap-2 mb-1"> <Label>Request Variables</Label> <HelpTooltip text="Add variables to be used in the API call. E.g. IDs that can change for each request." /> </div> <Textarea value={formData.inputPayload} onChange={handleChange('inputPayload')} placeholder="Enter JSON payload structure" className="font-mono h-32" /> </div> )} </div> </TabsContent> </Tabs> </div> )} {step === 'try_and_output' && ( <div className="space-y-2 h-full"> <InteractiveExtractPlayground configId={extractId} instruction={formData.instruction} onInstructionChange={handleChange('instruction')} responseSchema={formData.responseSchema} onResponseSchemaChange={handleChange('responseSchema')} initialRawResponse={initialRawResponse} responseMapping={responseMapping} onMappedResponse={handleMappedResponse} onRun={handleRun} isRunning={isRunning} mappedResponseData={mappedResponseData} hideRunButton={true} hideInstruction={true} file={file} /> </div> )} {step === 'success' && ( <div className="space-y-4 h-full"> <p className="text-m font-medium">Done!</p> <p className="text-sm font-medium">Your extraction is complete. You can now download the results or use the code examples below.</p> <Tabs defaultValue="download" className="w-full"> <TabsList className="grid w-full grid-cols-3"> <TabsTrigger value="download">Download Result</TabsTrigger> <TabsTrigger value="curl">cURL</TabsTrigger> <TabsTrigger value="sdk">TypeScript SDK</TabsTrigger> </TabsList> <TabsContent value="download"> <div className="rounded-md bg-muted p-4"> <div className="flex items-start space-x-2"> <Upload className="mt-0.5 h-5 w-5 text-muted-foreground shrink-0" /> <div className="space-y-3 w-full"> <p className="text-sm font-medium">Download the extracted data in your preferred format:</p> <div className="flex gap-3"> <Button variant="outline" onClick={() => downloadData(mappedResponseData, 'json')} className="flex-1 bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-200/50 hover:border-blue-300/50 text-blue-600 hover:text-blue-700" > <div className="flex items-center justify-center gap-2"> <Terminal className="h-4 w-4" /> Download JSON </div> </Button> <Button variant="outline" onClick={() => downloadData(mappedResponseData, 'csv')} className="flex-1 bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-200/50 hover:border-green-300/50 text-green-600 hover:text-green-700" > <div className="flex items-center justify-center gap-2"> <Download className="h-4 w-4" /> Download CSV </div> </Button> </div> </div> </div> </div> </TabsContent> <TabsContent value="curl"> <div className="rounded-md bg-muted p-4"> <div className="flex items-start space-x-2"> <Terminal className="mt-0.5 h-5 w-5 text-muted-foreground shrink-0" /> <div className="space-y-1 w-full"> <div className="flex items-center justify-between"> <p className="text-sm font-medium">Try the endpoint locally with curl: </p> <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => { navigator.clipboard.writeText(getCurlCommand()); }} > <Copy className="h-4 w-4" /> </Button> </div> <div className="relative"> <pre className="rounded-lg bg-secondary p-4 text-sm overflow-x-auto whitespace-pre-wrap break-all"> <code>{getCurlCommand()}</code> </pre> </div> </div> </div> </div> </TabsContent> <TabsContent value="sdk"> <div className="rounded-md bg-muted p-4"> <div className="flex items-start space-x-2"> <Terminal className="mt-0.5 h-5 w-5 text-muted-foreground shrink-0" /> <div className="space-y-1 w-full"> <div className="flex items-center justify-between"> <p className="text-sm font-medium">Or use the TypeScript SDK in your application: </p> <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => { navigator.clipboard.writeText(getSdkCode()); }} > <Copy className="h-4 w-4" /> </Button> </div> <div className="relative"> <pre className="rounded-lg bg-secondary p-4 text-sm overflow-x-auto whitespace-pre-wrap break-all"> <code>{getSdkCode()}</code> </pre> </div> </div> </div> </div> </TabsContent> </Tabs> </div> )} </div> <div className="flex-none mt-2 sm:mt-4 flex flex-col lg:flex-row gap-2 justify-between"> {step === 'success' ? ( <> <Button variant="outline" onClick={handleBack} > Back </Button> <div className="flex gap-2"> {/* not implemented yet <Button variant="outline" onClick={() => { router.push(`/configs/${extractId}/edit`) onOpenChange(false) }} > Advanced Edit </Button> */} <Button onClick={() => { router.push('/configs') onOpenChange(false) }} > Done </Button> </div> </> ) : ( <> <Button variant="outline" onClick={handleBack} disabled={step === 'basic'} > Back </Button> <Button onClick={step === 'try_and_output' && !mappedResponseData ? handleRun : handleNext} disabled={isAutofilling || (step === 'try_and_output' && !mappedResponseData && isRunning)} > {isAutofilling ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> {step as StepperStep === 'basic' ? 'superglue extracts file...' : 'Configuring...'} </> ) : ( step === 'try_and_output' ? (isRunning ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Running... </> ) : (!mappedResponseData ? ( <> ✨ Run </> ) : 'Complete')) : 'Next' )} </Button> </> )} </div> </DialogContent> </Dialog> ) }

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