Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
ApiConfigForm.tsx36.2 kB
'use client'; import { useConfig } from '@/src/app/config-context'; import ApiConfigIdEditModal from '@/src/components/api/ApiConfigIdEditModal'; import { ApiPlayground } from '@/src/components/api/ApiPlayground'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/src/components/ui/alert-dialog"; import { Button } from "@/src/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/src/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/src/components/ui/dialog"; import { Input } from "@/src/components/ui/input"; import { Label } from "@/src/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/src/components/ui/tabs"; import { Textarea } from "@/src/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/src/components/ui/tooltip"; import JsonSchemaEditor from "@/src/components/utils/JsonSchemaEditor"; import { useToast } from "@/src/hooks/use-toast"; import { isJsonEmpty } from '@/src/lib/client-utils'; import { ApiConfig, AuthType, CacheMode, HttpMethod, PaginationType, SuperglueClient } from '@superglue/client'; import { ArrowLeft, Info } from 'lucide-react'; import { useRouter } from 'next/navigation'; import Prism from 'prismjs'; import 'prismjs/components/prism-json'; import React from 'react'; import Editor from 'react-simple-code-editor'; const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; const AUTH_TYPES = ['NONE', 'HEADER', 'QUERY_PARAM', 'OAUTH2']; const PAGINATION_TYPES = ['OFFSET_BASED', 'PAGE_BASED', 'CURSOR_BASED', 'DISABLED']; const InfoTooltip = ({ text }: { text: string; }) => ( <TooltipProvider delayDuration={100}> <Tooltip> <TooltipTrigger type="button"> <Info className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" /> </TooltipTrigger> <TooltipContent> <p className="max-w-xs text-sm">{text}</p> </TooltipContent> </Tooltip> </TooltipProvider> ); const highlightJson = (code: string) => { try { const formatted = JSON.stringify(JSON.parse(code), null, 2); return Prism.highlight(formatted, Prism.languages.json, 'json'); } catch { return code; } }; const ApiConfigForm = ({ id }: { id?: string; }) => { const router = useRouter(); const [searchParamsChecked, setSearchParamsChecked] = React.useState(false); const { toast } = useToast(); const [formData, setFormData] = React.useState({ id: '', urlHost: '', urlPath: '', method: 'auto', instruction: '', queryParams: '', headers: '', body: '', documentationUrl: '', responseSchema: '', responseMapping: '', dataPath: '', authentication: 'auto', paginationType: 'auto', pageSize: '' }); const [isAutofilling, setIsAutofilling] = React.useState(false); const [editingId, setEditingId] = React.useState(id); const [isAutofillDialogOpen, setIsAutofillDialogOpen] = React.useState(false); const [autofillCredentials, setAutofillCredentials] = React.useState("{}"); const [isEditingIdModalOpen, setIsEditingIdModalOpen] = React.useState(false); const superglueConfig = useConfig(); const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false); const [showUnsavedChangesDialog, setShowUnsavedChangesDialog] = React.useState(false); const [activeTab, setActiveTab] = React.useState("basic"); React.useEffect(() => { if (editingId && !searchParamsChecked) { fetchConfig(); } }, [searchParamsChecked]); // Only depend on searchParamsChecked const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.urlHost || !formData.instruction) { toast({ title: "Missing Required Fields", description: "Please provide both Host and Instruction before saving", variant: "destructive", }); return; } // Add JSON validation for headers and query parameters try { if (formData.headers && formData.headers.trim()) { JSON.parse(formData.headers); } } catch (error) { toast({ title: "Invalid Headers JSON", description: "Please check your headers format", variant: "destructive", }); setActiveTab("request"); return; } try { if (formData.queryParams && formData.queryParams.trim()) { JSON.parse(formData.queryParams); } } catch (error) { toast({ title: "Invalid Query Parameters JSON", description: "Please check your query parameters format", variant: "destructive", }); setActiveTab("request"); return; } if (!formData.id) { formData.id = formData.urlHost .replace(/^(https?|postgres(ql)?|ftp(s)?|sftp|file):\/\//, '') // Remove http:// or https:// .replace(/\//g, '') // Remove all slashes + (formData.urlPath ? formData.urlPath.split('/').pop() : '') + '-' + Math.floor(1000 + Math.random() * 9000); } try { const payload = { id: formData.id, ...buildEndpointConfig(formData), createdAt: new Date(), updatedAt: new Date(), } as ApiConfig; const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }); const response = await superglueClient.upsertApi(formData.id, payload); if (!response) { throw new Error("Failed to save configuration"); } setHasUnsavedChanges(false); router.push(`/configs/${response.id}/edit`); // Add refetch after successful save await fetchConfig(); toast({ title: "Configuration Saved", description: "Configuration saved successfully", variant: "default", }); } catch (error) { console.error('Error saving config:', error); toast({ title: "Error Saving Configuration", description: "An error occurred while saving the configuration: " + error, variant: "destructive", }); } }; const handleChange = (field: string) => ( e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string ) => { const value = typeof e === 'string' ? e : e.target.value; setFormData(prev => { const newState = { ...prev, [field]: value }; return newState; }); setHasUnsavedChanges(true); if (field === 'id') { setEditingId(value); } }; const handleApiRun = (runConfig: ApiConfig) => { // Update form data based on the config used in the playground run setFormData(prev => ({ ...prev, // Keep existing form data id: runConfig.id || prev.id, // Update ID if changed (though unlikely here) urlHost: runConfig.urlHost || prev.urlHost, urlPath: runConfig.urlPath || prev.urlPath, method: runConfig.method || prev.method, // instruction: runConfig.instruction || prev.instruction, // Instruction likely doesn't change on run queryParams: runConfig.queryParams ? JSON.stringify(runConfig.queryParams, null, 2) : prev.queryParams, headers: runConfig.headers ? JSON.stringify(runConfig.headers, null, 2) : prev.headers, body: runConfig.body || prev.body, // documentationUrl: runConfig.documentationUrl || prev.documentationUrl, // Doc URL unlikely to change responseSchema: runConfig.responseSchema ? JSON.stringify(runConfig.responseSchema, null, 2) : prev.responseSchema, responseMapping: runConfig.responseMapping || prev.responseMapping, dataPath: runConfig.dataPath || prev.dataPath, authentication: runConfig.authentication || prev.authentication, paginationType: runConfig.pagination?.type || prev.paginationType, pageSize: runConfig.pagination?.pageSize ? String(runConfig.pagination.pageSize) : prev.pageSize, })); }; const handleAutofill = async () => { // Check for required fields setIsAutofillDialogOpen(true); }; const fetchConfig = async () => { try { const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }); const data = await superglueClient.getApi(editingId); setFormData({ id: data.id, urlHost: data.urlHost, instruction: data.instruction, urlPath: data.urlPath || '', method: data.method || 'auto', queryParams: JSON.stringify(data.queryParams || {}, null, 2), headers: JSON.stringify(data.headers || {}, null, 2), body: data.body || '', documentationUrl: data.documentationUrl || '', responseSchema: JSON.stringify(data.responseSchema || {}, null, 2), responseMapping: data.responseMapping || '', dataPath: data.dataPath || '', authentication: data.authentication || 'auto', paginationType: data.pagination?.type || 'auto', pageSize: String(data.pagination?.pageSize || "") }); setSearchParamsChecked(true); } catch (error) { console.error('Error fetching config:', error); } }; const buildEndpointConfig = (formData: Record<string, any>): ApiConfig => { const config: ApiConfig = { id: formData.id, urlHost: formData.urlHost, instruction: formData.instruction, urlPath: formData.urlPath, headers: isJsonEmpty(formData.headers) ? null : JSON.parse(formData.headers), queryParams: isJsonEmpty(formData.queryParams) ? null : JSON.parse(formData.queryParams), body: formData.body, dataPath: formData.dataPath, method: formData.method !== "auto" ? formData.method as HttpMethod : null, authentication: formData.authentication !== "auto" ? formData.authentication as AuthType : null, responseSchema: isJsonEmpty(formData.responseSchema) ? null : JSON.parse(formData.responseSchema), responseMapping: formData.responseMapping ? String(formData.responseMapping) : null, documentationUrl: formData.documentationUrl, pagination: formData.paginationType !== "auto" ? { type: formData.paginationType as PaginationType, pageSize: formData.pageSize || null } : null }; return config; }; const handleAutofillSubmit = async () => { if (!formData.urlHost || !formData.instruction) { toast({ title: "Missing Required Fields", description: "Please provide both Host and Instruction before autofilling", variant: "destructive", }); return; } setIsAutofilling(true); try { let parsedCredentials = {}; try { parsedCredentials = JSON.parse(autofillCredentials); } catch (e) { toast({ title: "Invalid JSON", description: "Please check your payload and credentials JSON format", variant: "destructive", }); return; } const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }); const response = await superglueClient.call({ endpoint: buildEndpointConfig(formData), credentials: parsedCredentials, options: { cacheMode: CacheMode.DISABLED } }); if (response.error) { throw new Error(response.error); } // Apply the returned config to the form const config = response.config as ApiConfig; if (config) { setFormData({ id: formData.id || config.id || '', urlHost: config.urlHost || '', urlPath: config.urlPath || '', method: config.method || 'auto', instruction: config.instruction || '', queryParams: JSON.stringify(config.queryParams || {}, null, 2), headers: JSON.stringify(config.headers || {}, null, 2), body: config.body || '', documentationUrl: config.documentationUrl || '', responseSchema: JSON.stringify(config.responseSchema || {}, null, 2), responseMapping: config.responseMapping || '', dataPath: config.dataPath || '', authentication: config.authentication || 'auto', paginationType: config.pagination?.type || 'auto', pageSize: String(config.pagination?.pageSize || '') }); } setIsAutofillDialogOpen(false); } catch (error: any) { console.error('Error during autofill:', error); toast({ title: "Autofill Failed", description: error?.message || "An error occurred while autofilling the configuration", variant: "destructive", }); } finally { setIsAutofilling(false); } }; const handleTabChange = (value: string) => { if (value === "test" && hasUnsavedChanges) { setShowUnsavedChangesDialog(true); } else { setActiveTab(value); } }; const handleSaveAndRun = async () => { try { // Reuse existing submit logic const payload = { id: formData.id, ...buildEndpointConfig(formData), createdAt: new Date(), updatedAt: new Date(), } as ApiConfig; const superglueClient = new SuperglueClient({ endpoint: superglueConfig.superglueEndpoint, apiKey: superglueConfig.superglueApiKey }); await superglueClient.upsertApi(formData.id, payload); setHasUnsavedChanges(false); setShowUnsavedChangesDialog(false); setActiveTab("test"); toast({ title: "Configuration Saved", description: "Configuration saved successfully", variant: "default", }); } catch (error) { console.error('Error saving config:', error); toast({ title: "Error Saving Configuration", description: "An error occurred while saving the configuration: " + error, variant: "destructive", }); } }; const handleConfigIdUpdated = (newId: string) => { router.push(`/configs/${newId}/edit`); }; return ( <div className="p-8 max-w-none w-full min-h-full"> <Button variant="ghost" onClick={() => router.push('/configs')} className="mb-4" > <ArrowLeft className="mr-2 h-4 w-4" /> Back </Button> <form onSubmit={handleSubmit} className="h-full"> <Card className="mb-6 h-[calc(100vh-12rem)]"> <CardHeader> <CardTitle>{editingId ? 'Edit' : 'Create'} API Configuration</CardTitle> <CardDescription> Configure the API endpoint and its behavior </CardDescription> </CardHeader> <CardContent className="h-[calc(100%-5rem)]"> <Tabs value={activeTab} onValueChange={handleTabChange} className="w-full h-full"> <TabsList className="mb-4"> <TabsTrigger value="basic">Basic Info</TabsTrigger> <TabsTrigger value="request">Request</TabsTrigger> <TabsTrigger value="responseMapping">Response Mapping</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger> {editingId && ( <TabsTrigger value="test"> Run </TabsTrigger> )} </TabsList> <div className="overflow-y-auto h-[calc(100%-4rem)]"> <TabsContent value="basic" className="h-full m-0"> <div className="grid grid-cols-2 gap-4 h-full"> <div className="space-y-4"> <div> <Label htmlFor="id" className="flex items-center gap-1 my-1"> ID <InfoTooltip text="A unique identifier for this API configuration. Use this ID to call the endpoint through the SDK." /> </Label> <div className="flex items-center"> <Input id="id" value={formData.id} onChange={handleChange('id')} placeholder="unique-identifier" disabled={!!editingId} className={`${editingId ? "disabled:opacity-100" : ""} ${editingId ? "rounded-r-none" : ""}`} /> {editingId && ( <Button type="button" variant="outline" className="h-9 rounded-l-none" onClick={() => setIsEditingIdModalOpen(true)} > <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-pen" > <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /> </svg> <span className="sr-only">Edit ID</span> </Button> )} </div> </div> <div> <Label htmlFor="urlHost" className="flex items-center gap-1 my-1"> Host <InfoTooltip text="The base URL of the API endpoint (e.g., https://api.example.com)" /> </Label> <Input id="urlHost" value={formData.urlHost} onChange={handleChange('urlHost')} placeholder="https://api.example.com" required /> </div> <div className="h-[calc(100%-10.8rem)]"> <Label htmlFor="instruction" className="flex items-center gap-1 my-1"> Instruction <InfoTooltip text="Describe what this API does and what data it should return. Be as specific as possible." /> </Label> <Textarea id="instruction" value={formData.instruction} onChange={handleChange('instruction')} placeholder="Get a list of all available products." className="h-full" required /> </div> </div> <div className="h-full overflow-y-hidden"> <JsonSchemaEditor value={formData.responseSchema} onChange={handleChange('responseSchema')} /> </div> </div> </TabsContent> <TabsContent value="request"> <div className="grid gap-4"> <div> <Label htmlFor="urlPath" className="flex items-center gap-1 my-1"> URL Path <InfoTooltip text="The path component of the URL, starting with / (e.g., /v1/products)" /> </Label> <Input id="urlPath" value={formData.urlPath} onChange={handleChange('urlPath')} placeholder="/rest/v1/products" /> </div> <div> <Label htmlFor="method" className="flex items-center gap-1 my-1"> HTTP Method <InfoTooltip text="The HTTP method to use for the request. Select 'Auto' to let the system determine the appropriate method." /> </Label> <Select value={formData.method} onValueChange={handleChange('method')} > <SelectTrigger> <SelectValue placeholder="Auto" /> </SelectTrigger> <SelectContent> <SelectItem value="auto">Auto</SelectItem> {HTTP_METHODS.map(method => ( <SelectItem key={method} value={method}> {method} </SelectItem> ))} </SelectContent> </Select> </div> <div> <Label htmlFor="headers" className="flex items-center gap-1 my-1"> Headers (JSON) <InfoTooltip text="Request headers in JSON format. Each key-value pair represents a header name and its value." /> </Label> <div className="relative font-mono border rounded-md"> <Editor value={formData.headers} onValueChange={handleChange('headers')} highlight={highlightJson} padding={10} tabSize={2} insertSpaces={true} className="min-h-[8rem] text-sm [&_textarea]:outline-none [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:p-0 [&_textarea]:border-0 [&_textarea]:bg-transparent dark:[&_textarea]:text-white" /> </div> </div> <div> <Label htmlFor="queryParams" className="flex items-center gap-1 my-1"> Query Parameters (JSON) <InfoTooltip text="URL query parameters in JSON format. These will be appended to the URL as ?key=value pairs." /> </Label> <div className="relative font-mono border rounded-md"> <Editor value={formData.queryParams} onValueChange={handleChange('queryParams')} highlight={highlightJson} padding={10} tabSize={2} insertSpaces={true} className="min-h-[8rem] text-sm [&_textarea]:outline-none [&_textarea]:w-full [&_textarea]:resize-none [&_textarea]:p-0 [&_textarea]:border-0 [&_textarea]:bg-transparent dark:[&_textarea]:text-white" /> </div> </div> <div> <Label htmlFor="body" className="flex items-center gap-1 my-1"> Request Body <InfoTooltip text="The request body to send with POST, PUT, or PATCH requests. Usually in JSON format." /> </Label> <Textarea id="body" value={formData.body} onChange={handleChange('body')} placeholder="Request body..." className="h-32 font-mono" /> </div> </div> </TabsContent> <TabsContent value="responseMapping"> <div className="grid gap-4"> <div> <Label htmlFor="dataPath" className="flex items-center gap-1 my-1"> Data Path <InfoTooltip text="JSON path to the array of items in the response (e.g., 'data.items' for nested data)" /> </Label> <Input id="dataPath" value={formData.dataPath} onChange={handleChange('dataPath')} placeholder="products" className="font-mono" /> </div> <div> <Label htmlFor="responseMapping" className="flex items-center gap-1 my-1"> Response Mapping (JSONata) <InfoTooltip text="JSONata expression to transform the API response into the desired format" /> </Label> <Textarea id="responseMapping" value={formData.responseMapping} onChange={handleChange('responseMapping')} placeholder="$.data" className="h-48 font-mono" /> </div> </div> </TabsContent> <TabsContent value="advanced"> <div className="grid gap-4"> <div> <Label htmlFor="documentationUrl" className="flex items-center gap-1 my-1"> Documentation URL <InfoTooltip text="Link to the API documentation for reference" /> </Label> <Input id="documentationUrl" value={formData.documentationUrl} onChange={handleChange('documentationUrl')} placeholder="https://docs.example.com" /> </div> <div className="grid gap-4 pt-4"> <h3 className="font-medium">Authentication</h3> <div className="grid grid-cols-2 gap-4"> <div> <Label htmlFor="authentication" className="flex items-center gap-1 my-1"> Authentication <InfoTooltip text="The authentication method required by the API. Select 'Auto' to let the system detect it." /> </Label> <Select value={formData.authentication} onValueChange={handleChange('authentication')} > <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="auto">Auto</SelectItem> {AUTH_TYPES.map(type => ( <SelectItem key={type} value={type}> {type} </SelectItem> ))} </SelectContent> </Select> </div> </div> </div> <div className="grid gap-4 pt-4"> <h3 className="font-medium">Pagination</h3> <div className="grid grid-cols-2 gap-4"> <div> <Label htmlFor="paginationType" className="flex items-center gap-1 my-1"> Type <InfoTooltip text="The pagination method used by the API. Select 'Auto' for automatic detection, or 'Disabled' if the API doesn't support pagination." /> </Label> <Select value={formData.paginationType} onValueChange={handleChange('paginationType')} > <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="auto">Auto</SelectItem> {PAGINATION_TYPES.map(type => ( <SelectItem key={type} value={type}> {type} </SelectItem> ))} </SelectContent> </Select> </div> <div> <Label htmlFor="pageSize" className="flex items-center gap-1 my-1"> Page Size <InfoTooltip text="Number of items to request per page when using pagination" /> </Label> <Input id="pageSize" type="number" value={formData.pageSize} onChange={handleChange('pageSize')} min="1" disabled={formData.paginationType === 'DISABLED'} /> </div> </div> </div> </div> </TabsContent> {editingId && ( <TabsContent value="test" className="-mx-6 max-w-full"> {hasUnsavedChanges ? ( <div className="p-4 text-center text-muted-foreground"> Please save your changes before running the configuration. </div> ) : ( <ApiPlayground configId={editingId} onRunApi={handleApiRun} // <-- Pass the handler here /> )} </TabsContent> )} </div> </Tabs> </CardContent> </Card> <div className="flex justify-end gap-4"> <Button type="button" variant="outline" onClick={() => router.push('/configs')}> Cancel </Button> <Button type="button" onClick={handleAutofill} disabled={isAutofilling} > {isAutofilling ? ( <span className="flex items-center"> <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Generating... </span> ) : ( 'Autofill Configuration' )} </Button> <Button type="submit"> {editingId ? 'Save' : 'Create'} Configuration </Button> </div> </form> <Dialog open={isAutofillDialogOpen} onOpenChange={setIsAutofillDialogOpen}> <DialogContent className="max-w-2xl"> <DialogHeader> <DialogTitle>Autofill Configuration</DialogTitle> <DialogDescription> Provide credentials and payload to automatically generate the configuration </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> <div> <Label htmlFor="autofillHost">Host</Label> <Input id="autofillHost" value={formData.urlHost} onChange={handleChange('urlHost')} placeholder="https://api.example.com" /> </div> <div> <Label htmlFor="autofillInstruction">Instruction</Label> <Textarea id="autofillInstruction" value={formData.instruction} onChange={handleChange('instruction')} placeholder="Describe what this API configuration does..." rows={3} /> </div> <div> <Label htmlFor="autofillDocUrl">Documentation URL</Label> <Input id="autofillDocUrl" value={formData.documentationUrl} onChange={handleChange('documentationUrl')} placeholder="https://docs.example.com" /> </div> <div> <Label htmlFor="autofillCredentials">Credentials (JSON)</Label> <Textarea id="autofillCredentials" className="font-mono" value={autofillCredentials} onChange={(e) => setAutofillCredentials(e.target.value)} placeholder="{}" rows={6} /> </div> </div> <div className="flex justify-end gap-4"> <Button type="button" variant="outline" onClick={() => setIsAutofillDialogOpen(false)} > Cancel </Button> <Button type="button" onClick={handleAutofillSubmit} disabled={isAutofilling} > {isAutofilling ? ( <span className="flex items-center"> <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Generating... </span> ) : ( 'Generate Configuration' )} </Button> </div> </DialogContent> </Dialog> <AlertDialog open={showUnsavedChangesDialog} onOpenChange={setShowUnsavedChangesDialog} > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Save Configuration?</AlertDialogTitle> <AlertDialogDescription> You have unsaved changes. You need to save them to run the configuration. Would you like to save the configuration now? </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={handleSaveAndRun}> Save & Run </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> {editingId && ( <ApiConfigIdEditModal isOpen={isEditingIdModalOpen} onClose={() => setIsEditingIdModalOpen(false)} configId={editingId} onConfigUpdated={handleConfigIdUpdated} /> )} </div> ); }; export default ApiConfigForm;

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