Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
ConfigCreateStepper.tsx30.1 kB
'use client' import { useConfig } from '@/src/app/config-context'; import { useToast } from '@/src/hooks/use-toast'; import { parseCredentialsHelper, splitUrl } from '@/src/lib/client-utils'; import { cn, composeUrl, inputErrorStyles } from '@/src/lib/utils'; import { ApolloClient, gql, InMemoryCache, useSubscription } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { Label } from '@radix-ui/react-label'; import { ApiConfig, AuthType, CacheMode, SuperglueClient } from '@superglue/client'; import { integrations } from '@superglue/shared'; import { createClient } from 'graphql-ws'; import { Copy, Loader2, Terminal, Upload, X } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Textarea } from '../ui/textarea'; import { HelpTooltip } from '../utils/HelpTooltip'; import { API_CREATE_STEPS, StepIndicator, type StepperStep } from '../utils/StepIndicator'; import { URLField } from '../utils/URLField'; import { InteractiveApiPlayground } from './InteractiveApiPlayground'; interface ConfigCreateStepperProps { configId?: string mode?: 'create' | 'edit' prefillData?: { fullUrl: string instruction: string documentationUrl?: string } onComplete?: () => void } export function ConfigCreateStepper({ configId: initialConfigId, mode = 'create', prefillData, onComplete }: ConfigCreateStepperProps) { const [step, setStep] = useState<StepperStep>('basic') const [isAutofilling, setIsAutofilling] = useState(false) const { toast } = useToast() const router = useRouter() const superglueConfig = useConfig() const [configId, setConfigId] = useState<string>(initialConfigId || '') 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({ fullUrl: prefillData?.fullUrl || '', instruction: prefillData?.instruction || '', documentationUrl: prefillData?.documentationUrl || '', inputPayload: '{}', auth: { type: AuthType.HEADER, value: '', }, responseSchema: '{}' }) const LOGS_SUBSCRIPTION = gql` subscription OnNewLog { logs { id message level timestamp runId } } ` const config = useConfig(); const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({}) const [docFile, setDocFile] = useState<File | null>(null) const [isDraggingDoc, setIsDraggingDoc] = useState(false) const [latestLog, setLatestLog] = useState<string>('') const client = useMemo(() => { const wsLink = new GraphQLWsLink(createClient({ url: config.superglueEndpoint?.replace('https', 'wss')?.replace('http', 'ws') || 'ws://localhost:3000/graphql', connectionParams: { Authorization: `Bearer ${config.superglueApiKey}` }, retryAttempts: Infinity, shouldRetry: () => true, retryWait: (retries) => new Promise((resolve) => setTimeout(resolve, Math.min(retries * 1000, 5000))), keepAlive: 10000, // Send keep-alive every 10 seconds })) return new ApolloClient({ link: wsLink, cache: new InMemoryCache(), defaultOptions: { watchQuery: { fetchPolicy: 'no-cache', }, query: { fetchPolicy: 'no-cache', }, }, }) }, [config.superglueEndpoint, config.superglueApiKey]) useEffect(() => { return () => { client.stop() } }, [client]) useSubscription(LOGS_SUBSCRIPTION, { client, shouldResubscribe: true, onError: (error) => { console.error('Subscription error:', error) }, onData: ({ data }) => { if (data.data?.logs) { setLatestLog(data.data.logs.message) } } }) 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) setResponseMapping(null) } } const handleAuthChange = (value: string) => { setFormData(prev => ({ ...prev, auth: { ...prev.auth, value } })) } const handleNext = async () => { if (step === 'basic') { const errors: Record<string, boolean> = {} if (!formData.fullUrl) { errors.fullUrl = 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({}) // Parse the URL when moving to the next step const url = splitUrl(formData.fullUrl) setIsAutofilling(true) try { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }) // Call autofill endpoint const response = await superglueClient.call({ endpoint: { id: url.urlHost?.replace(/^(https?|postgres(ql)?|ftp(s)?|sftp|file):\/\//, '').replace(/\//g, '') + '-' + Math.floor(1000 + Math.random() * 9000), urlHost: url.urlHost, ...(url.urlPath ? { urlPath: url.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 = url.urlHost?.replace(/^(https?|postgres(ql)?|ftp(s)?|sftp|file):\/\//, '').replace(/\//g, '') + '-' + Math.floor(1000 + Math.random() * 9000) setConfigId(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') } } } catch (error: any) { console.error('Error during autofill:', error) toast({ title: 'API Configuration Failed', description: error?.message || 'An error occurred while configuring the API', variant: 'destructive', duration: 10000 }) 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 url = splitUrl(formData.fullUrl) const savedConfig = await superglueClient.upsertApi(configId, { id: configId, urlHost: url.urlHost, instruction: formData.instruction, documentationUrl: formData.documentationUrl || undefined, responseMapping: responseMapping, responseSchema: JSON.parse(formData.responseSchema), createdAt: new Date(), updatedAt: new Date() } as ApiConfig) if (!savedConfig) { 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 handleManualCreate = () => { router.push('/configs/manual-new') } const handleClose = () => { if (mode === 'create') { router.push('/configs') } else { router.push(`/configs/${configId}/edit`) } } const getCurlCommand = () => { let payload = {} try { payload = JSON.parse(formData.inputPayload) } catch (e) { console.warn('Invalid input payload JSON') } const credentials = parseCredentialsHelper(formData.auth.value) const graphqlQuery = { query: `mutation CallApi($payload: JSON!, $credentials: JSON!) { call(input: { id: "${configId}" }, payload: $payload, credentials: $credentials) { data } }`, variables: { payload, credentials } } const command = `curl -X POST "${superglueConfig.superglueEndpoint}/graphql" \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${superglueConfig.superglueApiKey}" \\ -d '${JSON.stringify(graphqlQuery)}'` return command } 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}" }); // Transform any API response with a single call const result = await superglue.call({ id: "${configId}", payload: ${formData.inputPayload}, credentials: ${JSON.stringify(credentials)} })` } // 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.upsertApi(configId, { id: configId, instruction: formData.instruction, responseSchema: JSON.parse(formData.responseSchema), responseMapping: null }) // 2. Call the API using the config ID and get mapped response const mappedResult = await superglueClient.call({ id: configId, payload: JSON.parse(formData.inputPayload), credentials: parseCredentialsHelper(formData.auth.value) }) if (mappedResult.error) { throw new Error(mappedResult.error) } // 3. Set the mapped response const mappedData = mappedResult.data setMappedResponseData(mappedData) setHasMappedResponse(true) setResponseMapping((mappedResult.config as ApiConfig).responseMapping) } 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 these handlers for documentation file upload const handleDocDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDraggingDoc(true) } const handleDocDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDraggingDoc(false) } const extractTextFromFile = async (file: File): Promise<string> => { if (file.type === 'application/pdf') { // For PDFs, use pdf.js const pdfjsLib = await import('pdfjs-dist'); // Update worker path to use .mjs extension pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs'; const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let fullText = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const pageText = textContent.items.map((item: any) => item.str).join(' '); fullText += pageText + '\n'; } return fullText; } else { // For text files (.txt, .md, etc) return await file.text(); } } const handleDocDrop = async (e: React.DragEvent) => { e.preventDefault(); setIsDraggingDoc(false); const file = e.dataTransfer.files[0]; if (!file) return; try { const extractedText = await extractTextFromFile(file); setDocFile(file); // Store the extracted text in formData for later use setFormData(prev => ({ ...prev, documentationUrl: extractedText })); } catch (error) { console.error('Error extracting text from file:', error); toast({ title: 'Error Processing File', description: 'Could not extract text from the uploaded file', variant: 'destructive' }); } } const handleDocFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; try { const extractedText = await extractTextFromFile(file); setDocFile(file); // Store the extracted text in formData for later use setFormData(prev => ({ ...prev, documentationUrl: extractedText })); } catch (error) { console.error('Error extracting text from file:', error); toast({ title: 'Error Processing File', description: 'Could not extract text from the uploaded file', variant: 'destructive' }); } } // Update form data when prefillData changes or when modal is opened useEffect(() => { if (prefillData) { setFormData(prevData => ({ ...prevData, fullUrl: prefillData.fullUrl || prevData.fullUrl, instruction: prefillData.instruction || prevData.instruction, documentationUrl: prefillData.documentationUrl || prevData.documentationUrl })); } }, [prefillData]); useSubscription(LOGS_SUBSCRIPTION, { client, shouldResubscribe: true, onError: (error) => { console.error('Subscription error:', error) }, onData: ({ data }) => { if (data.data?.logs) { setLatestLog(data.data.logs.message) } } }) // Add a new function to handle URL changes from URLField const handleUrlChange = (urlHost: string, urlPath: string, queryParams: Record<string, string>) => { const fullUrl = urlHost + (urlPath || '') setFormData(prev => ({ ...prev, fullUrl })) // Auto-fill documentation URL if it's empty if (!formData.documentationUrl && urlHost) { // Check if URL matches any pattern in integrations const fullUrl = composeUrl(urlHost, urlPath) for (const pattern in integrations) { if (new RegExp(pattern).test(fullUrl)) { setFormData(prev => ({ ...prev, fullUrl, documentationUrl: integrations[pattern].docsUrl })) break } } } } return ( <div className="flex-1 flex flex-col h-full p-6"> <div className="flex-none mb-4"> <div className="flex flex-col lg:flex-row items-center justify-between gap-4 mb-4"> <h1 className="text-2xl font-semibold"> {step === 'success' ? 'Configuration Complete!' : 'Create New API Configuration'} </h1> <div className="flex items-center gap-2"> {!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> )} <Button variant="ghost" size="icon" className="shrink-0" onClick={() => { router.push('/configs') if (onComplete) { onComplete() } }} > <X className="h-4 w-4" /> </Button> </div> </div> <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="fullUrl">API Endpoint URL</Label> <HelpTooltip text="The API URL (e.g., https://api.example.com/v1). Don't include the endpoint, e.g. /books/list, we figure it out." /> </div> <URLField url={formData.fullUrl} onUrlChange={handleUrlChange} placeholder="https://api.example.com/v1" error={!!validationErrors.fullUrl} required /> {validationErrors.fullUrl && ( <p className="text-sm text-destructive mt-1">API endpoint URL is required</p> )} </div> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="documentationUrl">API Documentation</Label> <HelpTooltip text="Link to the API's documentation or upload a documentation file" /> </div> {docFile ? ( <div className="flex items-center gap-2"> <div className="flex-1 flex items-center gap-3 bg-blue-500/10 px-4 py-2 rounded-lg max-w-[calc(100%-5.5rem)]"> <Upload className="h-4 w-4 text-blue-500 shrink-0" /> <span className="text-white font-medium text-sm truncate">{formData.documentationUrl.slice(0, 300)}</span> </div> <Button variant="ghost" size="sm" className="shrink-0 text-muted-foreground hover:text-destructive transition-colors w-20" onClick={() => { setFormData(prev => ({ ...prev, documentationUrl: '' })) setDocFile(null) }} > Remove </Button> </div> ) : ( <div className="flex gap-2"> <Input id="documentationUrl" value={formData.documentationUrl} onChange={handleChange('documentationUrl')} placeholder="https://docs.example.com" className="flex-1" /> <div className={cn( "relative shrink-0", isDraggingDoc && "after:absolute after:inset-0 after:bg-primary/5 after:backdrop-blur-[1px] after:rounded-lg after:border-2 after:border-primary" )} onDragOver={handleDocDragOver} onDragLeave={handleDocDragLeave} onDrop={handleDocDrop} > <Button variant="outline" className="h-9" onClick={() => document.getElementById('doc-file-upload')?.click()} > Upload Document </Button> <input type="file" id="doc-file-upload" className="hidden" onChange={handleDocFileUpload} accept=".pdf,.txt,.md" /> </div> </div> )} </div> <div> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="auth">API Key or Token (Optional)</Label> <HelpTooltip text="Enter API secret here (not stored, just for initial setup). Omit prefixes like Bearer. Alternatively, you can put a JSON object here with multiple keys and values." /> </div> <Input id="auth" value={formData.auth.value} onChange={(e) => handleAuthChange(e.target.value)} placeholder="Enter your API key or token" /> </div> <div className="mt-6"> <div className="flex items-center gap-2 mb-1"> <Label htmlFor="instruction">What do you want to get from this API?</Label> <HelpTooltip text="Describe what data you want to extract from this API in plain English" /> </div> <Textarea id="instruction" value={formData.instruction} 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-48", validationErrors.instruction && inputErrorStyles, validationErrors.instruction && "focus:!border-destructive" )} required /> {validationErrors.instruction && ( <p className="text-sm text-destructive mt-1">Instruction is required</p> )} </div> </div> )} {step === 'try_and_output' && configId && ( <div className="space-y-2 h-full"> <InteractiveApiPlayground configId={configId} instruction={formData.instruction} onInstructionChange={handleChange('instruction')} responseSchema={formData.responseSchema} onResponseSchemaChange={handleChange('responseSchema')} initialRawResponse={initialRawResponse} onMappedResponse={handleMappedResponse} onRun={handleRun} isRunning={isRunning} mappedResponseData={mappedResponseData} responseMapping={responseMapping} hideRunButton={true} /> </div> )} {step === 'success' && ( <div className="space-y-4 h-full"> <p className="text-m font-medium">Done!</p> <p className="text-sm font-medium">You can now call the endpoint from your app. The call is proxied to the targeted endpoint without AI inbewteen. Predictable and millisecond latency.</p> <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 flex-none" 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> <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 flex-none" 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> </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"> <Button variant="outline" onClick={() => { router.push(`/configs/${configId}/edit`) }} > Advanced Edit </Button> <Button onClick={() => { router.push('/configs') // Call onComplete before closing if provided if (onComplete) { onComplete() } }} > Done </Button> </div> </> ) : ( <> <Button variant="outline" onClick={handleBack} disabled={step === 'basic'} > Back </Button> <div className='flex gap-2'> {step === 'basic' && <Button variant="outline" onClick={handleManualCreate} > Manual Configuration </Button> } <Button onClick={step === 'try_and_output' && !mappedResponseData ? handleRun : handleNext} disabled={isAutofilling || (step === 'try_and_output' && !mappedResponseData && isRunning)} className="animate-pulse" > {isAutofilling ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> {latestLog ? latestLog?.split(' ').slice(0, 4).join(' ').slice(0, 30) + '...' : 'Creating configuration...'} </> ) : ( step === 'try_and_output' ? (isRunning ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> {latestLog ? latestLog?.split(' ').slice(0, 4).join(' ').slice(0, 30) + '...' : 'Creating transformation...'} </> ) : (!mappedResponseData ? ( <> <span>✨ Run</span> </> ) : 'Complete')) : 'Next' )} </Button> </div> </> )} </div> </div> ) }

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