Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
JsonSchemaEditor.tsx27.8 kB
import { Button } from "@/src/components/ui/button"; 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 { Switch } from "@/src/components/ui/switch"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/src/components/ui/tooltip"; import { cn, MAX_DISPLAY_LINES, MAX_DISPLAY_SIZE } from "@/src/lib/utils"; import { Check, Copy, Plus, Trash2 } from "lucide-react"; import Prism from 'prismjs'; import 'prismjs/components/prism-json'; import React from 'react'; import Editor from 'react-simple-code-editor'; interface JsonSchemaEditorProps { value: string | null; onChange: (value: string | null) => void; isOptional?: boolean; title?: string; readOnly?: boolean; onBlur?: () => void; forceCodeMode?: boolean; showModeToggle?: boolean; } const SCHEMA_TYPES = ['object', 'string', 'number', 'boolean', 'integer', 'any', 'string[]', 'number[]', 'boolean[]', 'integer[]', 'object[]', 'any[]']; const SCHEMA_TYPE_DISPLAY = { 'object': 'object', 'string': 'string', 'number': 'number', 'boolean': 'bool', 'integer': 'integer', 'any': 'any', 'string[]': 'string[]', 'number[]': 'number[]', 'boolean[]': 'bool[]', 'integer[]': 'integer[]', 'object[]': 'object[]', 'any[]': 'any[]', }; const ARRAY_ITEM_TYPES = ['object', 'string', 'number', 'boolean', 'integer', 'any']; const ARRAY_ITEM_TYPE_DISPLAY = { 'object': 'object[]', 'string': 'string[]', 'number': 'number[]', 'boolean': 'bool[]', 'integer': 'integer[]', 'any': 'any[]', }; const JsonSchemaEditor: React.FC<JsonSchemaEditorProps> = ({ value, onChange, isOptional = false, readOnly = false, onBlur, forceCodeMode = false, showModeToggle = true, }) => { const [isCodeMode, setIsCodeMode] = React.useState(forceCodeMode || false); const [jsonError, setJsonError] = React.useState<string | null>(null); const [localIsEnabled, setLocalIsEnabled] = React.useState<boolean>(() => { if (!isOptional) return true; return value !== null && value !== '' && value !== undefined; }); React.useEffect(() => { if (forceCodeMode) { setIsCodeMode(true); return; } const savedMode = localStorage?.getItem('jsonSchemaEditorCodeMode'); if (savedMode !== null) { setIsCodeMode(savedMode === 'true'); } }, [forceCodeMode]); React.useEffect(() => { if (forceCodeMode) return; if (typeof window !== 'undefined') { localStorage.setItem('jsonSchemaEditorCodeMode', isCodeMode.toString()); } }, [isCodeMode, forceCodeMode]); React.useEffect(() => { if (isOptional) { const shouldBeEnabled = value !== null && value !== '' && value !== undefined; if (shouldBeEnabled !== localIsEnabled) { setLocalIsEnabled(shouldBeEnabled); } } }, [value, isOptional]); const [visualSchema, setVisualSchema] = React.useState<any>({}); const [editingField, setEditingField] = React.useState<string | null>(null); // Store field name during editing in case of duplicate keys const [tempFieldName, setTempFieldName] = React.useState<string>(""); // Track if current field name is a duplicate/invalid const [isDuplicateField, setIsDuplicateField] = React.useState(false); // Track the path of the currently hovered description field const [hoveredDescField, setHoveredDescField] = React.useState<string | null>(null); // Ref to store timeout IDs for hover delay const hoverTimeoutRef = React.useRef<{ [key: string]: NodeJS.Timeout }>({}); // Clear all hover timeouts when component unmounts React.useEffect(() => { return () => { for (const timeoutId of Object.values(hoverTimeoutRef.current)) { clearTimeout(timeoutId); } }; }, []); React.useEffect(() => { if (value === null) { setVisualSchema({}); setJsonError(null); return; } if (value === '') { setVisualSchema({}); setJsonError(null); return; } // Accept raw JSON strings like "{}" or {} and never re-wrap as quoted JSON strings try { const val = typeof value === 'string' ? value : String(value); const parsed = JSON.parse(val); setVisualSchema(parsed); setJsonError(null); // Do not auto-normalize content by writing back formatted value here; leave caret/typing intact } catch (e) { setJsonError((e as Error).message); } }, [value]); const updateVisualSchema = (path: string[], newValue: any) => { const newSchema = { ...visualSchema }; let current = newSchema; // Navigate to the target location for (let i = 0; i < path.length - 1; i++) { current = current[path[i]]; } const lastKey = path[path.length - 1]; const oldValue = current[lastKey]; // If updating type field, remove properties/items if (lastKey === 'type' && oldValue !== newValue) { if (current.properties) { current.properties = {}; } if (current.items) { current.items = {}; } } current[lastKey] = newValue; setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); }; // Handle mouse enter with delay const handleMouseEnter = (fieldId: string) => { // Clear any existing timeout for this field if (hoverTimeoutRef.current[fieldId]) { clearTimeout(hoverTimeoutRef.current[fieldId]); } // Set a new timeout to show tooltip after 2 seconds hoverTimeoutRef.current[fieldId] = setTimeout(() => { setHoveredDescField(fieldId); }, 1000); }; // Handle mouse leave const handleMouseLeave = (fieldId: string) => { // Clear the timeout if mouse leaves before delay completes if (hoverTimeoutRef.current[fieldId]) { clearTimeout(hoverTimeoutRef.current[fieldId]); delete hoverTimeoutRef.current[fieldId]; } // Hide the tooltip if it's currently shown for this field if (hoveredDescField === fieldId) { setHoveredDescField(null); } }; const generateUniqueFieldName = (properties: any) => { let index = 1; let fieldName = 'newField'; while (properties && properties[fieldName]) { fieldName = `newField${index}`; index++; } return fieldName; }; const renderSchemaField = (fieldName: string, schema: any, path: string[] = [], isArrayChild = false) => { if (!schema) return null; const isRoot = path.length === 0; const isEditing = editingField === path.join('.'); // Create a unique ID for this field's description const descFieldId = [...path, 'description'].join('.'); // Helper function to check if field is required const isFieldRequired = () => { if (isRoot || isArrayChild) return false; const parentPath = path.slice(0, -2); let parent = visualSchema; for (const segment of parentPath) { parent = parent[segment]; } return parent.required?.includes(fieldName) || false; }; const finishEditing = () => { // Only update the schema if the field name is not a duplicate and not empty if (!isDuplicateField && tempFieldName !== fieldName && tempFieldName.trim() !== '') { const newSchema = { ...visualSchema }; let current = newSchema; for (let i = 0; i < path.length - 2; i++) { current = current[path[i]]; } const properties = current[path[path.length - 2]]; const newProperties: Record<string, any> = {}; // Apply the name change for (const key of Object.keys(properties)) { if (key === fieldName) { newProperties[tempFieldName] = properties[fieldName]; } else { newProperties[key] = properties[key]; } } // Update required fields if needed if (current.required?.includes(fieldName)) { current.required = current.required.map((f: string) => f === fieldName ? tempFieldName : f ); } current[path[path.length - 2]] = newProperties; setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); } // Always reset states when done editing setTempFieldName(''); setEditingField(null); setIsDuplicateField(false); }; return ( <div key={fieldName} className="space-y-1 pl-2 border-l-2 border-gray-400 mb-2"> <div className="flex items-center gap-2"> {isEditing && !isArrayChild ? ( <Input // Use the temporary state for the input value without updating schema value={tempFieldName} onChange={(e) => { // Just update the temporary field name for visual display // WITHOUT modifying the actual schema until editing is complete setTempFieldName(e.target.value); let current = visualSchema; for (let i = 0; i < path.length - 2; i++) { current = current[path[i]]; } const properties = current[path[path.length - 2]]; // it's a duplicate if a property with that name exists and it's not the current field const isDuplicateKey = properties[e.target.value] && fieldName !== e.target.value; setIsDuplicateField(isDuplicateKey); // for UI highlighting // DO NOT update the schema while typing - only when editing is complete }} className={cn( "w-36 min-h-[32px] text-xs sm:text-sm", isDuplicateField && "border-red-500 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400" )} placeholder="Field name" autoFocus onBlur={finishEditing} onKeyDown={(e) => { if (e.key === 'Enter') { finishEditing(); } }} /> ) : (!isArrayChild && !isRoot && ( <div className={cn( "w-36 px-2 py-0.5 min-h-[32px] rounded hover:bg-secondary cursor-pointer flex items-center gap-0.5", "text-[11px] xs:text-[12px] sm:text-[14px] truncate" )} title={fieldName} onClick={() => { // Initialize the temp field name with the current field name setTempFieldName(fieldName); setEditingField(path.join('.')); // Reset duplicate field indicator when starting to edit setIsDuplicateField(false); }} > {isRoot ? 'root' : fieldName} {isFieldRequired() && ( <span className="text-red-500 text-[14px] sm:text-[18px] font-bold" title="Required field">*</span> )} </div> ))} {isRoot && ( <div className={cn( "w-36 px-2 py-0.5 min-h-[32px] rounded flex items-center gap-0.5", "text-[11px] xs:text-[12px] sm:text-[14px] text-muted-foreground" )} > root </div> )} <Select value={ isArrayChild ? (typeof schema.type === 'string' ? schema.type : 'any') : schema.type === 'array' && schema.items?.type ? `${schema.items.type}[]` : (typeof schema.type === 'string' ? schema.type : 'any') } onValueChange={(value) => { if (isArrayChild) { updateVisualSchema([...path, 'type'], value); } else if (value.endsWith('[]')) { // Handle array types like 'string[]' const itemType = value.slice(0, -2); if (isRoot) { // Handle root array type specially const newSchema = { type: 'array', items: { type: itemType } }; setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); } else { const newSchema = { ...visualSchema }; let current = newSchema; for (let i = 0; i < path.length - 1; i++) { current = current[path[i]]; } current[path[path.length - 1]] = { type: 'array', items: { type: itemType } }; setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); } } else { updateVisualSchema([...path, 'type'], value); } }} > <SelectTrigger className="w-28 h-8 text-xs sm:text-sm"> <SelectValue placeholder="Type" /> </SelectTrigger> <SelectContent className="text-xs sm:text-sm"> {(isArrayChild ? ARRAY_ITEM_TYPES : SCHEMA_TYPES).map(type => ( <SelectItem key={type} value={type}> {isArrayChild ? ARRAY_ITEM_TYPE_DISPLAY[type] : SCHEMA_TYPE_DISPLAY[type]} </SelectItem> ))} </SelectContent> </Select> <TooltipProvider> <Tooltip open={hoveredDescField === descFieldId}> <TooltipTrigger asChild> <Input value={schema.description || ''} onChange={(e) => { updateVisualSchema([...path, 'description'], e.target.value); // Clear any pending timeout if (hoverTimeoutRef.current[descFieldId]) { clearTimeout(hoverTimeoutRef.current[descFieldId]); delete hoverTimeoutRef.current[descFieldId]; } // Hide tooltip when typing setHoveredDescField(null); }} className="w-full min-w-[200px] flex-1 border-muted hover:border-primary/50 focus:border-primary/50 text-xs sm:text-sm" placeholder="Add AI instructions or filters" onFocus={() => { // Clear timeout and hide tooltip on focus if (hoverTimeoutRef.current[descFieldId]) { clearTimeout(hoverTimeoutRef.current[descFieldId]); delete hoverTimeoutRef.current[descFieldId]; } setHoveredDescField(null); }} onMouseEnter={() => handleMouseEnter(descFieldId)} onMouseLeave={() => handleMouseLeave(descFieldId)} /> </TooltipTrigger> <TooltipContent side="top"> <span className="text-xs sm:text-sm">Add instructions to help AI understand how to map data to this field</span> </TooltipContent> </Tooltip> </TooltipProvider> {!isRoot && !isArrayChild && ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <div className="relative z-10"> <Switch className="custom-switch" id={`required-${path.join('-')}`} checked={isFieldRequired()} onCheckedChange={(checked) => { const parentPath = path.slice(0, -2); const newSchema = { ...visualSchema }; let parent = newSchema; for (const segment of parentPath) { parent = parent[segment]; } if (checked) { parent.required = [...(parent.required || []), fieldName]; } else { parent.required = (parent.required || []).filter((f: string) => f !== fieldName); if (parent.required.length === 0) { delete parent.required; } } setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); }} /> <span className="pointer-events-none absolute flex h-4 w-4 items-center justify-center rounded-full bg-white shadow-lg ring-0 transition-transform peer-data-[state=checked]:translate-x-4 peer-data-[state=unchecked]:translate-x-0 left-[2px] top-[2px]"> {isFieldRequired() ? ( <span className="text-red-500 text-[12px] sm:text-[16px] font-bold leading-none mt-2">*</span> ) : ( <span className="text-muted-foreground text-[10px] sm:text-[12px] font-semibold leading-none">?</span> )} </span> </div> </TooltipTrigger> <TooltipContent side="top" className="z-50 text-xs sm:text-sm"> {isFieldRequired() ? 'Make field optional' : 'Make field required'} </TooltipContent> </Tooltip> </TooltipProvider> )} {!isRoot && path.length > 0 && ( <Button variant="ghost" size="icon" className="text-destructive h-8 w-8 min-w-[2rem] min-h-[2rem]" onClick={() => { const newSchema = { ...visualSchema }; let current = newSchema; // Navigate to the parent to handle required fields const parentPath = path.slice(0, -2); let parent = newSchema; for (const segment of parentPath) { parent = parent[segment]; } // If this field was required, remove it from the required array if (parent.required?.includes(fieldName)) { parent.required = parent.required.filter((f: string) => f !== fieldName); // Remove the required array if it's empty if (parent.required.length === 0) { delete parent.required; } } // Delete the field itself for (let i = 0; i < path.length - 1; i++) current = current[path[i]]; delete current[path[path.length - 1] === 'properties' ? fieldName : path[path.length - 1]]; setVisualSchema(newSchema); onChange(JSON.stringify(newSchema, null, 2)); }} > <Trash2 className="h-4 w-4" /> </Button> )} </div> {schema.type === 'object' && ( <div className={`pl-2 ${!isRoot && 'mt-1'}`}> <div className="overflow-x-auto"> {schema.properties && Object.entries(schema.properties).map(([key, value]) => renderSchemaField(key, value, [...path, 'properties', key]) )} </div> <Button type="button" variant="outline" size="sm" className="gap-2 text-xs sm:text-sm" onClick={() => updateVisualSchema( [...path, 'properties'], { ...(schema.properties || {}), [generateUniqueFieldName(schema.properties)]: { type: 'string' } } )} > <Plus className="h-3 w-3 sm:h-4 sm:w-4" /> <span className="text-xs sm:text-sm">Add Property</span> </Button> </div> )} {schema.type === 'array' && schema.items?.type === 'object' && ( <div className="pl-2 mt-1"> <div className="overflow-x-auto"> {schema.items.properties && Object.entries(schema.items.properties).map(([key, value]) => renderSchemaField(key, value, [...path, 'items', 'properties', key]) )} </div> <Button type="button" variant="outline" size="sm" className="gap-2 text-xs sm:text-sm" onClick={() => updateVisualSchema( [...path, 'items', 'properties'], { ...(schema.items.properties || {}), [generateUniqueFieldName(schema.items.properties)]: { type: 'string' } } )} > <Plus className="h-3 w-3 sm:h-4 sm:w-4" /> <span className="text-xs sm:text-sm">Add Property</span> </Button> </div> )} </div> ); }; const ensureObjectStructure = (schema: any) => { if (!schema.type) schema.type = 'any'; if (!schema.properties) schema.properties = {}; return schema; }; // Add this highlight function const highlightJson = (code: string) => { try { return Prism.highlight(code, Prism.languages.json, 'json'); } catch { return code; } }; const handleEnabledChange = (enabled: boolean) => { if (isOptional) { setLocalIsEnabled(enabled); if (!enabled) { onChange(null); } else { // If enabling and current parent value is null or empty, provide a default // This ensures a fresh start if re-enabled if (value === null || value === '') { onChange(JSON.stringify(ensureObjectStructure({ type: 'any' }), null, 2)); } // Otherwise keep the existing value } } }; const shouldShowHeader = (showModeToggle && (localIsEnabled || !isOptional)) || isOptional; return ( <div className="space-y-1 flex flex-col h-full mb-4 gap-2"> {shouldShowHeader && ( <div className="flex items-center gap-4 ml-auto shrink-0"> <div className="flex items-center gap-4"> {showModeToggle && (localIsEnabled || !isOptional) && ( <div className="flex items-center gap-2"> <Label htmlFor="editorMode" className="text-xs">Code Mode</Label> <Switch className="custom-switch" id="editorMode" checked={isCodeMode} onCheckedChange={setIsCodeMode} /> </div> )} {isOptional && ( <div className="flex items-center gap-2"> <Label htmlFor="schemaOptionalToggle" className="text-xs">Enabled</Label> <Switch className="custom-switch" id="schemaOptionalToggle" checked={localIsEnabled} onCheckedChange={handleEnabledChange} /> </div> )} </div> </div> )} {isCodeMode && jsonError && ( <div className="p-2 bg-destructive/10 border border-destructive/20 text-destructive text-xs rounded-md"> {jsonError} </div> )} {isCodeMode && readOnly && value && (value.length > MAX_DISPLAY_SIZE || (value.split('\n').length > MAX_DISPLAY_LINES)) && ( <div className="p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 text-xs rounded-md text-amber-800 dark:text-amber-200"> Preview may be truncated for performance </div> )} {localIsEnabled ? ( <div className="flex-1 min-h-0 border rounded-md min-h-32"> {isCodeMode ? ( <div className="h-full font-mono relative bg-transparent"> <div className="overflow-auto h-full"> <div className="absolute top-1 right-1 z-10"> <IconCopyButton text={value ?? ''} /> </div> <Editor value={value ?? ''} onValueChange={(code) => { onChange(code || ''); try { if (code === '' || code === null) { setJsonError(null); } else { JSON.parse(code); setJsonError(null); } } catch (e) { setJsonError((e as Error).message); } }} highlight={highlightJson} padding={10} tabSize={2} insertSpaces={true} disabled={readOnly} onBlur={onBlur} className={cn( "font-mono text-xs w-full min-h-full border-0 border-transparent outline-none focus:outline-none focus-visible:outline-0 ring-0 focus:ring-0 focus-visible:ring-0" )} textareaClassName="border-0 border-transparent focus:border-0 focus:border-transparent outline-none focus:outline-none focus-visible:outline-0 ring-0 focus:ring-0 focus-visible:ring-0" style={{ fontFamily: 'var(--font-mono)', minHeight: '100%', outline: 'none', boxShadow: 'none', border: '0', }} /> </div> </div> ) : ( <div className="h-full flex flex-col min-h-0"> {Object.keys(visualSchema).length === 0 ? ( <div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <p className="mb-4 text-xs sm:text-sm mt-6">No schema defined yet</p> <Button variant="outline" className="text-xs sm:text-sm" onClick={() => { const initialSchema = ensureObjectStructure({ type: 'any' }); setVisualSchema(initialSchema); onChange(JSON.stringify(initialSchema, null, 2)); }} > <span className="text-xs sm:text-sm">Add Root Property</span> </Button> </div> ) : ( <div className="flex-1 overflow-y-auto min-h-0"> <div className="p-4"> <div className="overflow-x-auto"> {renderSchemaField('root', visualSchema, [])} </div> </div> </div> )} </div> )} </div> ) : ( <div className="flex-grow min-h-[4rem] p-3 border rounded-md text-sm text-muted-foreground bg-muted/50 flex items-center justify-center"> Schema definition is disabled. </div> )} </div> ); }; export default JsonSchemaEditor; const IconCopyButton = ({ text }: { text: string }) => { const [copied, setCopied] = React.useState(false); const handleCopy = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); navigator.clipboard.writeText(text || ''); setCopied(true); setTimeout(() => setCopied(false), 1500); }; return ( <button onClick={handleCopy} className="h-6 w-6 flex items-center justify-center rounded hover:bg-background/80 transition-colors bg-background/60 backdrop-blur" title="Copy" type="button" > {copied ? <Check className="h-3 w-3 text-green-600" /> : <Copy className="h-3 w-3 text-muted-foreground" />} </button> ); };

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