Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
IntegrationForm.tsx60.1 kB
import { useConfig } from '@/src/app/config-context'; import { detectAuthType } from '@/src/app/integrations/page'; import { Button } from '@/src/components/ui/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/src/components/ui/card'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/src/components/ui/command'; import { Input } from '@/src/components/ui/input'; import { Label } from '@/src/components/ui/label'; import { Popover, PopoverContent, PopoverTrigger } from '@/src/components/ui/popover'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/src/components/ui/select'; import { Switch } from '@/src/components/ui/switch'; import { Textarea } from '@/src/components/ui/textarea'; import { CredentialsManager } from '@/src/components/utils/CredentialManager'; import { DocumentationField } from '@/src/components/utils/DocumentationField'; import { HelpTooltip } from '@/src/components/utils/HelpTooltip'; import { URLField } from '@/src/components/utils/URLField'; import { useToast } from '@/src/hooks/use-toast'; import { cn, composeUrl, inputErrorStyles } from '@/src/lib/utils'; import type { Integration } from '@superglue/client'; import { createOAuthErrorHandler, getOAuthCallbackUrl, triggerOAuthFlow } from '@/src/lib/oauth-utils'; import { integrations } from '@superglue/shared'; import { Check, ChevronRight, ChevronsUpDown, Copy, Globe } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; export interface IntegrationFormProps { integration?: Integration; onSave: (integration: Integration) => Promise<Integration | null>; onCancel: () => void; integrationOptions: { value: string; label: string; icon: string }[]; getSimpleIcon: (name: string) => any; modal?: boolean; } function sanitizeIntegrationId(id: string) { // Handle PostgreSQL connection strings specially if (id.startsWith('postgres://') || id.startsWith('postgresql://')) { try { const url = new URL(id); let host = url.hostname; const database = url.pathname.substring(1); // Remove leading slash // Truncate host if too long (keep first 20 chars) if (host.length > 20) { host = host.substring(0, 20); } let cleanId = `DB-${host}-${database}`; return cleanId .toLowerCase() .replace(/[^a-z0-9-]/g, '-') // Replace non-alphanumeric with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens } catch { } } let cleanId = id.replace(/^.*:\/\//, ''); const slashIndex = cleanId.indexOf('/'); if (slashIndex !== -1) { cleanId = cleanId.substring(0, slashIndex); } return cleanId .toLowerCase() .replace(/[^a-z0-9-]/g, '-') // Replace non-alphanumeric with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens } export function IntegrationForm({ integration, onSave, onCancel, integrationOptions, getSimpleIcon, modal = false, }: IntegrationFormProps) { const initialSelected = integration ? integrationOptions.find(opt => opt.value !== '' && (integration.id === opt.value || (integration.urlHost && integration.urlHost.includes(opt.value))) )?.value || '' : ''; const [selectedIntegration, setSelectedIntegration] = useState<string>(initialSelected); const [integrationDropdownOpen, setIntegrationDropdownOpen] = useState(false); const [id, setId] = useState(integration?.id || initialSelected); const [urlHost, setUrlHost] = useState(integration?.urlHost || ''); const [urlPath, setUrlPath] = useState(integration?.urlPath || ''); const [documentationUrl, setDocumentationUrl] = useState(integration?.documentationUrl || ''); const [documentation, setDocumentation] = useState(integration?.documentation || ''); const [specificInstructions, setSpecificInstructions] = useState(integration?.specificInstructions || ''); // Add state to track if user manually edited the ID const [isIdManuallyEdited, setIsIdManuallyEdited] = useState(false); // Initialize auth type const initialAuthType = !integration ? 'apikey' : detectAuthType(integration.credentials || {}); const [authType, setAuthType] = useState<'none' | 'oauth' | 'apikey'>(initialAuthType); // Initialize OAuth fields const [oauthFields, setOauthFields] = useState(() => { const creds = integration?.credentials || {}; return { client_id: creds.client_id || '', client_secret: creds.client_secret || '', auth_url: creds.auth_url || '', token_url: creds.token_url || '', access_token: creds.access_token || '', refresh_token: creds.refresh_token || '', scopes: creds.scopes || '', expires_at: creds.expires_at || '', token_type: creds.token_type || 'Bearer', grant_type: 'authorization_code' }; }); // Track initial OAuth field values to detect changes const [initialOAuthFields, setInitialOAuthFields] = useState(() => { const creds = integration?.credentials || {}; return { client_id: creds.client_id || '', client_secret: creds.client_secret || '', auth_url: creds.auth_url || '', token_url: creds.token_url || '', scopes: creds.scopes || '', grant_type: 'authorization_code' }; }); // Track initial API credentials to detect changes const [initialApiCredentials, setInitialApiCredentials] = useState(() => { const creds = integration?.credentials || {}; if (initialAuthType === 'oauth' && integration) { const { client_id, client_secret, auth_url, token_url, access_token, refresh_token, scopes, expires_at, token_type, grant_type, ...additionalCreds } = creds; return Object.keys(additionalCreds).length > 0 ? JSON.stringify(additionalCreds, null, 2) : '{}'; } return Object.keys(creds).length > 0 ? JSON.stringify(creds, null, 2) : '{}'; }); // Initialize API key credentials as JSON string for CredentialsManager const [apiKeyCredentials, setApiKeyCredentials] = useState(() => { const creds = integration?.credentials || {}; // For OAuth integrations, only include non-OAuth fields in the additional credentials if (initialAuthType === 'oauth' && integration) { const { client_id, client_secret, auth_url, token_url, access_token, refresh_token, scopes, expires_at, token_type, grant_type, ...additionalCreds } = creds; return Object.keys(additionalCreds).length > 0 ? JSON.stringify(additionalCreds, null, 2) : '{}'; } return Object.keys(creds).length > 0 ? JSON.stringify(creds, null, 2) : '{}'; }); const [validationErrors, setValidationErrors] = useState<Record<string, boolean>>({}); const [showAdvanced, setShowAdvanced] = useState(false); const [showOAuth, setShowOAuth] = useState(false); const [useSuperglueOAuth, setUseSuperglueOAuth] = useState<boolean>(false); const [connectLoading, setConnectLoading] = useState(false); const [hasUploadedFile, setHasUploadedFile] = useState( // Check if existing integration has file upload integration?.documentationUrl?.startsWith('file://') || false ); const { toast } = useToast(); useEffect(() => { const handleOAuthMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin) return; const messageIntegrationId = event.data?.integrationId; if (!messageIntegrationId || (integration?.id && messageIntegrationId !== integration.id && messageIntegrationId !== id)) { return; } if (event.data?.type === 'oauth-success') { setConnectLoading(false); const tokens = event.data.tokens as { access_token?: string; refresh_token?: string; token_type?: string; expires_at?: string } | undefined; if (tokens) { setOauthFields(prev => ({ ...prev, access_token: tokens.access_token || prev.access_token, refresh_token: tokens.refresh_token || prev.refresh_token, token_type: tokens.token_type || prev.token_type, expires_at: tokens.expires_at || prev.expires_at, })); // Show success feedback for client_credentials flow if (oauthFields.grant_type === 'client_credentials') { toast({ title: 'OAuth Connected', description: 'Successfully authenticated with client credentials', }); } } } else if (event.data?.type === 'oauth-error') { setConnectLoading(false); } }; window.addEventListener('message', handleOAuthMessage); return () => window.removeEventListener('message', handleOAuthMessage); }, [id, integration?.id, oauthFields.grant_type, toast]); const urlFieldRef = useRef<any>(null); const isEditing = !!integration; const config = useConfig(); // Pre-fill OAuth fields when auth type/template changes; enable SG toggle if template has client_id useEffect(() => { if (authType !== 'oauth' || !selectedIntegration) return; const integrationTemplate = integrations[selectedIntegration]; const templateOAuth: any = integrationTemplate?.oauth; setUseSuperglueOAuth(prev => (isEditing ? prev : !!(templateOAuth?.client_id && String(templateOAuth.client_id).trim().length > 0))); setOauthFields(prev => ({ ...prev, client_id: prev.client_id || (templateOAuth?.client_id || ''), auth_url: prev.auth_url || (templateOAuth?.authUrl || ''), token_url: prev.token_url || (templateOAuth?.tokenUrl || ''), scopes: prev.scopes || (templateOAuth?.scopes || ''), grant_type: (templateOAuth?.grant_type as 'authorization_code' | 'client_credentials') || prev.grant_type || 'authorization_code' })); }, [authType, selectedIntegration]); useEffect(() => { if (!isEditing || authType !== 'oauth' || !selectedIntegration) return; const templateClientId = integrations[selectedIntegration]?.oauth?.client_id; const savedClientId = (integration?.credentials || {}).client_id || oauthFields.client_id; if (templateClientId && savedClientId && savedClientId === templateClientId) { setUseSuperglueOAuth(true); } }, [isEditing, authType, selectedIntegration, integration, oauthFields.client_id]); // Function to handle file upload const handleFileUpload = (extractedText: string) => { setDocumentation(extractedText); setHasUploadedFile(true); }; // Function to handle file removal const handleFileRemove = () => { setDocumentation(''); setHasUploadedFile(false); }; const handleIntegrationSelect = (value: string) => { setSelectedIntegration(value); if (!value) { setUrlHost(''); setUrlPath(''); setDocumentationUrl(''); setDocumentation(''); setSpecificInstructions(''); // Set custom as ID if not editing if (!isEditing) { setId('new-integration'); } return; } // Use integrations from shared package const integrationTemplate = integrations[value]; if (integrationTemplate) { const apiUrl = integrationTemplate.apiUrl || ''; let urlHost = ''; let urlPath = ''; try { const url = new URL(apiUrl); urlHost = url.origin; urlPath = url.pathname; } catch { urlHost = apiUrl; urlPath = ''; } setUrlHost(urlHost); setUrlPath(urlPath); // Only set documentation URL if no file is uploaded if (!hasUploadedFile) { setDocumentationUrl(integrationTemplate.docsUrl || ''); setDocumentation(''); } setSpecificInstructions(''); // Always set ID to dropdown value for new integrations if (!isEditing) { setId(value); } if (integrationTemplate.preferredAuthType) { setAuthType(integrationTemplate.preferredAuthType); } // Pre-fill OAuth fields if OAuth is preferred/available if (integrationTemplate.oauth && integrationTemplate.preferredAuthType === 'oauth') { const templateOAuth: any = integrationTemplate.oauth; setOauthFields(prev => ({ ...prev, client_id: templateOAuth?.client_id || prev.client_id, auth_url: templateOAuth?.authUrl || prev.auth_url, token_url: templateOAuth?.tokenUrl || prev.token_url, scopes: templateOAuth?.scopes || prev.scopes || '', grant_type: (templateOAuth?.grant_type as 'authorization_code' | 'client_credentials') || prev.grant_type || 'authorization_code' })); setUseSuperglueOAuth(!!(templateOAuth?.client_id && String(templateOAuth.client_id).trim().length > 0)); } else { setUseSuperglueOAuth(false); } } }; const handleUrlChange = (host: string, path: string) => { setUrlHost(host); setUrlPath(path); // Auto-update ID when URL changes (only for new integrations and if not manually edited) if (!isEditing && !isIdManuallyEdited) { const fullUrl = composeUrl(host, path); if (fullUrl) { const sanitizedId = sanitizeIntegrationId(fullUrl); if (sanitizedId) { setId(sanitizedId); } } } }; // Check if OAuth fields have changed const hasOAuthFieldsChanged = () => { if (authType !== 'oauth') return false; // Check if any OAuth field changed const oauthFieldsChanged = oauthFields.client_id !== initialOAuthFields.client_id || oauthFields.client_secret !== initialOAuthFields.client_secret || oauthFields.auth_url !== initialOAuthFields.auth_url || oauthFields.token_url !== initialOAuthFields.token_url || oauthFields.scopes !== initialOAuthFields.scopes || oauthFields.grant_type !== initialOAuthFields.grant_type; // Check if additional API credentials changed const apiCredentialsChanged = apiKeyCredentials !== initialApiCredentials; return oauthFieldsChanged || apiCredentialsChanged; }; const getTemplateOAuth = () => { if (!selectedIntegration) return null as any; return (integrations[selectedIntegration]?.oauth as any) || null; }; const buildEffectiveOAuthFields = (): typeof oauthFields => { const templateOAuth = getTemplateOAuth(); if (useSuperglueOAuth && templateOAuth) { return { ...oauthFields, client_id: templateOAuth.client_id || oauthFields.client_id, auth_url: templateOAuth.authUrl || oauthFields.auth_url, token_url: templateOAuth.tokenUrl || oauthFields.token_url, scopes: templateOAuth.scopes || oauthFields.scopes, grant_type: (templateOAuth.grant_type as 'authorization_code' | 'client_credentials') || oauthFields.grant_type || 'authorization_code' }; } return oauthFields; }; const effectiveGrantType = oauthFields.grant_type || 'authorization_code'; const effectiveAccessToken = (integration?.credentials as any)?.access_token || oauthFields.access_token; const effectiveRefreshToken = (integration?.credentials as any)?.refresh_token || oauthFields.refresh_token; const isOAuthConfigured: boolean = effectiveGrantType === 'client_credentials' ? Boolean(effectiveAccessToken) : Boolean(effectiveAccessToken && effectiveRefreshToken); const isConnectDisabled = () => { if (authType !== 'oauth') return true; if (useSuperglueOAuth) return false; const ef = buildEffectiveOAuthFields(); const isClientCreds = ef.grant_type === 'client_credentials'; if (isClientCreds) { return !(ef.client_id && ef.client_secret && ef.token_url); } return !(ef.client_id && ef.client_secret && ef.token_url && ef.auth_url && ef.scopes); }; const handleConnect = async () => { if (authType !== 'oauth') return; setConnectLoading(true); const ef = buildEffectiveOAuthFields(); const errors: Record<string, boolean> = {}; if (!useSuperglueOAuth) { const isClientCreds = ef.grant_type === 'client_credentials'; if (!ef.client_id) errors.client_id = true; if (!ef.client_secret) errors.client_secret = true; if (!ef.token_url) errors.token_url = true; if (!isClientCreds) { if (!ef.auth_url) errors.auth_url = true; if (!ef.scopes) errors.scopes = true; } setValidationErrors(prev => ({ ...prev, ...errors })); if (Object.keys(errors).length > 0) { setConnectLoading(false); return; } } let creds: any = {}; try { const additional = JSON.parse(apiKeyCredentials || '{}'); creds = { ...ef, ...additional }; } catch { creds = { ...ef }; } const integrationData = { id: isEditing ? integration!.id : (id || '').trim(), urlHost: urlHost.trim(), urlPath: urlPath.trim(), documentationUrl: documentationUrl.trim(), documentation: documentation.trim(), specificInstructions: specificInstructions.trim(), credentials: creds, } as Integration; try { const templateInfo = useSuperglueOAuth ? { templateId: selectedIntegration, clientId: ef.client_id } : undefined; const handleOAuthError = (error: string) => { setConnectLoading(false); const errorHandler = createOAuthErrorHandler(integrationData.id, toast); errorHandler(error); }; const handleOAuthSuccess = (tokens: any) => { setConnectLoading(false); if (tokens) { setOauthFields(prev => ({ ...prev, access_token: tokens.access_token || prev.access_token, refresh_token: tokens.refresh_token || prev.refresh_token, token_type: tokens.token_type || prev.token_type, expires_at: tokens.expires_at || prev.expires_at, })); toast({ title: 'OAuth Connected', description: 'Successfully authenticated', }); } }; triggerOAuthFlow( integrationData.id, ef, selectedIntegration, config.superglueApiKey, authType, handleOAuthError, true, templateInfo, handleOAuthSuccess, config.superglueEndpoint ); } catch (error) { console.error('Error connecting OAuth:', error); setConnectLoading(false); } }; const handleSubmit = async () => { const errors: Record<string, boolean> = {}; if (!id.trim()) errors.id = true; if (!urlHost.trim()) errors.urlHost = true; if (specificInstructions.length > 10000) errors.specificInstructions = true; let creds = {}; // Build credentials based on auth type if (authType === 'oauth') { const ef = buildEffectiveOAuthFields(); const { grant_type: _omitGrantType, ...efWithoutGrant } = ef as any; const oauthCredsRaw = Object.fromEntries( Object.entries({ ...efWithoutGrant, scopes: efWithoutGrant.scopes || integrations[selectedIntegration]?.oauth?.scopes || '' }).filter(([_, value]) => value !== '') ) as Record<string, any>; let additionalCreds: Record<string, any> = {}; try { additionalCreds = JSON.parse(apiKeyCredentials); } catch { } creds = { ...oauthCredsRaw, ...additionalCreds }; } else if (authType === 'apikey') { try { creds = JSON.parse(apiKeyCredentials); } catch { setValidationErrors(prev => ({ ...prev, credentials: true })); return; } } setValidationErrors(errors); if (Object.keys(errors).length > 0) return; // Create the integration object const integrationData = { id: isEditing ? integration!.id : id.trim(), urlHost: urlHost.trim(), urlPath: urlPath.trim(), documentationUrl: documentationUrl.trim(), documentation: documentation.trim(), specificInstructions: specificInstructions.trim(), credentials: creds, }; try { // Save the integration first and get the resolved integration ID back from backend const savedIntegration = await onSave(integrationData); if (!savedIntegration) { toast({ title: 'Error', description: 'Failed to save integration', variant: 'destructive', }); return; } } catch (error) { } }; // Helper to copy text to clipboard const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); toast({ title: 'Copied', description: 'Callback URL copied to clipboard', }); } catch (err) { toast({ title: 'Failed to copy', description: 'Please copy the URL manually', variant: 'destructive', }); } }; return ( <Card className={cn( modal ? "border-0 shadow-none bg-background" : "mt-4 border-primary/50", "flex flex-col max-h-[calc(100vh-200px)]" )}> <CardHeader className={cn( modal ? "p-6 pb-0 border-0" : "py-3 px-4", "flex-shrink-0" )}> <CardTitle className="text-lg">{integration ? 'Edit Integration' : 'Add New Integration'}</CardTitle> </CardHeader> <CardContent className={cn( modal ? "p-6 space-y-3 border-0" : "p-4 space-y-3", "flex-1 overflow-y-auto" )}> <div> <Label htmlFor="integrationSelect">Integration Template</Label> <HelpTooltip text="Choose a pre-configured template to get started quickly, or select 'Custom API' to set up any API manually. Templates are optional but make setup easier." /> <Popover open={integrationDropdownOpen} onOpenChange={setIntegrationDropdownOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={integrationDropdownOpen} className="w-full justify-between" > <div className="flex items-center gap-2"> {selectedIntegration ? ( <> {(() => { const icon = getSimpleIcon( integrationOptions.find(opt => opt.value === selectedIntegration)?.icon || '' ); return icon ? ( <svg width="16" height="16" viewBox="0 0 24 24" fill={`#${icon.hex}`} className="flex-shrink-0" > <path d={icon.path} /> </svg> ) : ( <Globe className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> ); })()} <span> {integrationOptions.find(option => option.value === selectedIntegration)?.label} </span> </> ) : ( <span>Select integration...</span> )} </div> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0 pointer-events-auto z-[9999]"> <Command className="w-full"> <CommandInput placeholder="Search templates..." /> <CommandEmpty> <div className="text-center py-4"> <div className="text-sm text-muted-foreground mb-2"> No template found </div> <div className="text-xs text-muted-foreground"> Don't worry! You can still create this integration using "Custom API" </div> </div> </CommandEmpty> <CommandGroup className="max-h-[300px] overflow-y-auto"> {integrationOptions.map((option) => ( <CommandItem key={option.value} value={option.value} onSelect={() => { handleIntegrationSelect(option.value); setIntegrationDropdownOpen(false); }} className="flex items-center py-2" > <div className="flex items-center gap-2 w-full"> <div className="w-6 flex justify-center"> {(() => { const icon = getSimpleIcon(option.icon); return icon ? ( <svg width="16" height="16" viewBox="0 0 24 24" fill={`#${icon.hex}`} className="flex-shrink-0" > <path d={icon.path} /> </svg> ) : ( <Globe className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> ); })()} </div> <span className="flex-grow">{option.label}</span> <Check className={cn( 'h-4 w-4 flex-shrink-0', selectedIntegration === option.value ? 'opacity-100' : 'opacity-0' )} /> </div> </CommandItem> ))} </CommandGroup> </Command> </PopoverContent> </Popover> </div> {!isEditing && ( <div> <Label htmlFor="integrationId">Integration ID*</Label> <HelpTooltip text="A unique identifier for this integration within the tool. You cannot change this after saving." /> <Input id="integrationId" value={id || ''} onChange={e => { setId(e.target.value); setIsIdManuallyEdited(true); }} placeholder="e.g., crm-api" className={cn(validationErrors.id && inputErrorStyles)} /> {validationErrors.id && <p className="text-sm text-destructive mt-1">Integration ID is required and must be unique.</p>} </div> )} <div> <Label htmlFor="integrationFullUrl">API Endpoint*</Label> <HelpTooltip text="The base URL of the API (e.g., https://api.example.com/v1)." /> <URLField ref={urlFieldRef} url={composeUrl(urlHost, urlPath) || ''} onUrlChange={handleUrlChange} error={validationErrors.urlHost} /> {validationErrors.urlHost && <p className="text-sm text-destructive mt-1">API Endpoint is required.</p>} </div> <div> <Label htmlFor="documentation">Documentation</Label> <HelpTooltip text="You can either paste a documentation URL or upload a file. To add multiple pages, you can upload a zip file. You can add manual documentation to the instructions in the advanced options below." /> <DocumentationField url={documentationUrl || ''} content={documentation || ''} onUrlChange={setDocumentationUrl} onContentChange={(content) => { setDocumentation(content); }} onFileUpload={handleFileUpload} onFileRemove={handleFileRemove} hasUploadedFile={hasUploadedFile} /> </div> <div className="space-y-4"> <div> <Label htmlFor="authType" className="flex items-center gap-2"> Authentication Type <HelpTooltip text="Choose how to authenticate with this API. OAuth is recommended for supported services." /> </Label> <Select key={authType} value={authType} onValueChange={(value: 'apikey' | 'oauth' | 'none') => setAuthType(value)} > <SelectTrigger className="w-full"> <SelectValue placeholder="Select authentication type" /> </SelectTrigger> <SelectContent> <SelectItem value="apikey">API Key / Token / Basic Auth</SelectItem> <SelectItem value="oauth">OAuth 2.0</SelectItem> <SelectItem value="none">No Authentication</SelectItem> </SelectContent> </Select> </div> {authType === 'oauth' && ( <> {!isOAuthConfigured && !useSuperglueOAuth && ( <div className="inline-flex items-center gap-1 text-amber-800 dark:text-amber-300 text-xs bg-amber-500/10 px-2 py-1.5 rounded border border-amber-500/20"> <svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"> <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> </svg> <span>OAuth not configured. Add OAuth credentials and connect in OAuth setup below.</span> </div> )} {(useSuperglueOAuth || isOAuthConfigured) && ( <div className="pt-1"> <Button type="button" disabled={connectLoading} onClick={handleConnect} className="w-full h-11 justify-center text-base" > <span className="inline-flex items-center gap-2"> {(() => { const icon = getSimpleIcon( integrationOptions.find(opt => opt.value === selectedIntegration)?.icon || '' ); return icon ? ( <svg width="16" height="16" viewBox="0 0 24 24" fill={`#${icon.hex}`} className="flex-shrink-0" > <path d={icon.path} /> </svg> ) : ( <Globe className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> ); })()} {isOAuthConfigured ? ( <> <Check className="h-4 w-4 text-green-600" /> <span>Connected — Reauthenticate</span> </> ) : ( <span>Connect to {integrationOptions.find(option => option.value === selectedIntegration)?.label}</span> )} </span> </Button> </div> )} <div> <button type="button" onClick={() => setShowOAuth(!showOAuth)} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors" > <ChevronRight className={cn( "h-4 w-4 transition-transform", showOAuth && "rotate-90" )} /> OAuth Setup </button> </div> {showOAuth && ( <div className="space-y-4"> {(() => { const template = integrations[selectedIntegration as keyof typeof integrations]; const templateOAuth: any = template?.oauth || null; const hasTemplateClient = !!(templateOAuth?.client_id && String(templateOAuth.client_id).trim().length > 0); return hasTemplateClient ? ( <div className="flex items-center justify-between border rounded-md p-3"> <div className="text-sm"> <div className="font-medium">Use superglue OAuth client</div> <div className="text-xs text-muted-foreground">Preconfigured client for {integrationOptions.find(o => o.value === selectedIntegration)?.label}</div> </div> <Switch checked={useSuperglueOAuth} onCheckedChange={(v) => setUseSuperglueOAuth(Boolean(v))} className="custom-switch" /> </div> ) : null; })()} {!useSuperglueOAuth && ( <> <div className="grid grid-cols-2 gap-3"> <div> <Label htmlFor="client_id" className="text-xs flex items-center gap-1"> Client ID* <HelpTooltip text="Found in your OAuth provider app settings." /> </Label> <Input id="client_id" value={oauthFields.client_id} onChange={e => setOauthFields(prev => ({ ...prev, client_id: e.target.value }))} placeholder="Your OAuth app client ID" className={cn("h-9", validationErrors.client_id && inputErrorStyles)} autoComplete="off" /> </div> <div> <Label htmlFor="client_secret" className="text-xs flex items-center gap-1"> Client Secret* <HelpTooltip text="Secret from your OAuth provider app settings." /> </Label> <Input id="client_secret" type="password" value={oauthFields.client_secret} onChange={e => setOauthFields(prev => ({ ...prev, client_secret: e.target.value }))} placeholder="Your OAuth app client secret" className={cn("h-9", validationErrors.client_secret && inputErrorStyles)} autoComplete="new-password" /> </div> </div> <div> <Label htmlFor="grant_type" className="text-xs flex items-center gap-1"> OAuth Grant Type <HelpTooltip text="Authorization Code = user consent; Client Credentials = server-to-server." /> </Label> <Select value={oauthFields.grant_type} onValueChange={(value: 'authorization_code' | 'client_credentials') => setOauthFields(prev => ({ ...prev, grant_type: value })) } > <SelectTrigger className="h-9"> <SelectValue placeholder="Select grant type" /> </SelectTrigger> <SelectContent> <SelectItem value="authorization_code">Authorization Code</SelectItem> <SelectItem value="client_credentials">Client Credentials</SelectItem> </SelectContent> </Select> </div> {oauthFields.grant_type === 'authorization_code' && ( <div> <Label htmlFor="scopes" className="text-xs">OAuth Scopes*</Label> <HelpTooltip text="Space-separated scopes. Leave empty to use defaults." /> <Input id="scopes" value={oauthFields.scopes} onChange={e => setOauthFields(prev => ({ ...prev, scopes: e.target.value }))} placeholder="e.g., read write" className={cn("h-9", validationErrors.scopes && inputErrorStyles)} /> </div> )} <div className="grid grid-cols-2 gap-3"> <div> <Label htmlFor="auth_url" className="text-xs">Authorization URL{oauthFields.grant_type === 'authorization_code' ? '*' : ''}</Label> <HelpTooltip text="OAuth authorization endpoint." /> <Input id="auth_url" value={oauthFields.auth_url} onChange={e => setOauthFields(prev => ({ ...prev, auth_url: e.target.value }))} placeholder="OAuth authorization endpoint" className={cn("h-9", validationErrors.auth_url && inputErrorStyles)} /> </div> <div> <Label htmlFor="token_url" className="text-xs">Token URL*</Label> <HelpTooltip text="OAuth token endpoint." /> <Input id="token_url" value={oauthFields.token_url} onChange={e => setOauthFields(prev => ({ ...prev, token_url: e.target.value }))} placeholder="OAuth token endpoint" className={cn("h-9", validationErrors.token_url && inputErrorStyles)} /> </div> </div> {oauthFields.grant_type === 'authorization_code' && ( <div className="border-t pt-3"> <Label className="text-xs flex items-center gap-1"> Redirect URI <HelpTooltip text="Add this URL to your OAuth app allowed redirect URIs." /> </Label> <div className="flex items-center gap-2 mt-1 mb-2"> <code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto"> {getOAuthCallbackUrl()} </code> <Button type="button" variant="ghost" size="icon" className="h-8 w-8" onClick={() => copyToClipboard(getOAuthCallbackUrl())} > <Copy className="h-4 w-4" /> </Button> </div> </div> )} <div className="text-xs text-muted-foreground"> {oauthFields.grant_type === 'client_credentials' ? ( <span>Required: client ID, client secret, token URL.</span> ) : ( <span>Required: client ID, client secret, token URL, auth URL, scopes.</span> )} </div> </> )} {!useSuperglueOAuth && !isOAuthConfigured && ( <div className="pt-1"> <Button type="button" disabled={isConnectDisabled() || connectLoading} onClick={handleConnect} className="w-full h-11 justify-center text-base" > <span className="inline-flex items-center gap-2"> {isOAuthConfigured ? ( <> <Check className="h-4 w-4 text-green-600" /> <span>Connected — Reauthenticate</span> </> ) : ( <> Connect to {integrationOptions.find(option => option.value === selectedIntegration)?.label} {(() => { const icon = getSimpleIcon( integrationOptions.find(opt => opt.value === selectedIntegration)?.icon || '' ); return icon ? ( <svg width="16" height="16" viewBox="0 0 24 24" fill={`#${icon.hex}`} className="flex-shrink-0" > <path d={icon.path} /> </svg> ) : ( <Globe className="h-4 w-4 flex-shrink-0 text-muted-foreground" /> ); })()} </> )} </span> </Button> </div> )} </div> )} </> )} {authType === 'apikey' && ( <div className="space-y-2"> <Label htmlFor="credentials" className="flex items-center gap-2"> API Credentials <HelpTooltip text="Add API keys or tokens needed for this integration. You can add multiple key-value pairs. Common keys include: api_key, bearer_token, api_secret, username, password." /> </Label> <div className="w-full"> <CredentialsManager value={apiKeyCredentials} onChange={setApiKeyCredentials} className={cn('min-h-20', validationErrors.credentials && inputErrorStyles)} /> </div> {validationErrors.credentials && <p className="text-sm text-destructive">Credentials must be valid JSON.</p>} </div> )} {authType === 'none' && ( <div className="bg-muted/50"> <p className="text-sm text-muted-foreground"> The API endpoint should be publicly accessible or credentials should be provided at request time. </p> </div> )} </div> <div> <button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors" > <ChevronRight className={cn( "h-4 w-4 transition-transform", showAdvanced && "rotate-90" )} /> Advanced Options </button> </div> {showAdvanced && ( <> {authType === 'oauth' && ( <div className="space-y-2"> <Label htmlFor="additionalCredentials" className="flex items-center gap-2 text-xs"> Additional API Credentials <HelpTooltip text="Some APIs require additional credentials alongside OAuth. Common examples: developer_token (Google Ads), account_id, workspace_id. Add any extra key-value pairs needed." /> </Label> <div className="w-full"> <CredentialsManager value={apiKeyCredentials} onChange={setApiKeyCredentials} className={cn('min-h-20', validationErrors.credentials && inputErrorStyles)} /> </div> {validationErrors.credentials && <p className="text-sm text-destructive">Credentials must be valid JSON.</p>} </div> )} <div> <Label htmlFor="specificInstructions">Specific Instructions</Label> <HelpTooltip text="Provide specific guidance on how to use this integration (e.g., rate limits, special endpoints, authentication details). Max 2000 characters." /> <div className="relative"> <Textarea id="specificInstructions" value={specificInstructions} onChange={e => setSpecificInstructions(e.target.value)} placeholder="e.g. always use pagination with max 50 items per page" className={cn( 'min-h-[100px] pr-16', validationErrors.specificInstructions && inputErrorStyles )} maxLength={10000} /> <div className="absolute bottom-2 right-2 text-xs text-muted-foreground"> {specificInstructions.length}/10000 </div> </div> {validationErrors.specificInstructions && ( <p className="text-sm text-destructive mt-1"> Specific instructions must be 10000 characters or less. </p> )} </div> </> )} </CardContent> <CardFooter className={cn( modal ? "p-6 pt-0 border-0" : "p-4 pt-2", "flex-shrink-0" )}> <div className="flex justify-end gap-2 w-full"> <Button variant="outline" onClick={onCancel}>Cancel</Button> <Button onClick={handleSubmit}> {integration ? 'Save Changes' : 'Add Integration'} </Button> </div> </CardFooter> </Card> ); } export default IntegrationForm;

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