Skip to main content
Glama

Convex MCP server

Official
by get-convex
EnvironmentVariables.tsx25 kB
import { Form, Formik, getIn, useFormikContext } from "formik"; import { ClipboardCopyIcon, Cross2Icon, EyeNoneIcon, EyeOpenIcon, Pencil1Icon, PlusIcon, TrashIcon, } from "@radix-ui/react-icons"; import classNames from "classnames"; import { ClipboardEventHandler, useEffect, useId, useRef, useState, } from "react"; import { z } from "zod"; import { Spinner } from "@ui/Spinner"; import { Callout } from "@ui/Callout"; import { Button } from "@ui/Button"; import { copyTextToClipboard, toast } from "@common/lib/utils"; import { TextInput } from "@ui/TextInput"; const MAX_NUMBER_OF_ENV_VARS = 100; export const ENVIRONMENT_VARIABLES_ROW_CLASSES = "grid grid-cols-[minmax(0,1fr)_7.5rem] gap-4 py-2 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_7.5rem] items-start"; export const ENVIRONMENT_VARIABLE_NAME_COLUMN = "col-span-2 md:col-span-1"; const ERROR_ENV_VAR_NOT_UNIQUE = "Environment variable name is not unique"; export type BaseEnvironmentVariable = { name: string; value: string }; type FormState<T extends BaseEnvironmentVariable> = { editedVars: { oldEnvVar: T; newEnvVar: BaseEnvironmentVariable; }[]; newVars: BaseEnvironmentVariable[]; deletedVars: T[]; tooManyEnvVars: boolean; }; // This is used for showing both deployment environment variables and project level environment variables export function EnvironmentVariables<T extends BaseEnvironmentVariable>({ environmentVariables, updateEnvironmentVariables, initialFormValues, hasAdminPermissions, }: { environmentVariables: Array<T> | undefined; initialFormValues?: Array<BaseEnvironmentVariable>; updateEnvironmentVariables: ( creations: BaseEnvironmentVariable[], modifications: { oldEnvVar: T; newEnvVar: BaseEnvironmentVariable }[], deletions: T[], ) => Promise<void>; hasAdminPermissions: boolean; }) { return ( <Formik enableReinitialize initialValues={ { editedVars: [], newVars: initialFormValues ?? [], deletedVars: [], tooManyEnvVars: false, } as FormState<T> } onSubmit={async (values, helpers) => { await updateEnvironmentVariables( values.newVars, values.editedVars, values.deletedVars, ); helpers.resetForm({}); }} validate={(values) => { const errors: Record<string, string> = {}; // Names / values validation const uneditedVarNames = environmentVariables ?.filter( (v) => !values.editedVars.some((edited) => edited.oldEnvVar === v) && !values.deletedVars.some((deleted) => deleted === v), ) .map((sourceVar) => sourceVar.name) ?? []; const editedVarNames = values.editedVars.map( (editedVar) => editedVar.newEnvVar.name, ); const newVarNames = values.newVars.map((newVar) => newVar.name); const nameOccurrences = [ ...uneditedVarNames, ...editedVarNames, ...newVarNames, ].reduce( (acc, name) => acc.set(name, (acc.get(name) ?? 0) + 1), new Map(), ); const variablesToValidate: { value: BaseEnvironmentVariable; key: string; }[] = [ ...values.editedVars.map((editedVar, index) => ({ value: editedVar.newEnvVar, key: `editedVars[${index}].newEnvVar`, })), ...values.newVars.map((value, index) => ({ value, key: `newVars[${index}]`, })), ]; variablesToValidate.forEach(({ value, key }) => { try { EnvVarName.parse(value.name); } catch (err) { if (err instanceof z.ZodError) { errors[`${key}.name`] = err.issues[0].message; } } try { EnvVarValue.parse(value.value); } catch (err) { if (err instanceof z.ZodError) { errors[`${key}.value`] = err.issues[0].message; } } if (nameOccurrences.get(value.name) > 1) { errors[`${key}.name`] = ERROR_ENV_VAR_NOT_UNIQUE; } }); return errors; }} > <EnvironmentVariablesForm environmentVariables={environmentVariables} hasAdminPermissions={hasAdminPermissions} /> </Formik> ); } function EnvironmentVariablesForm<T extends BaseEnvironmentVariable>({ environmentVariables, hasAdminPermissions, }: { environmentVariables: Array<T> | undefined; hasAdminPermissions: boolean; }) { const formState = useFormikContext<FormState<T>>(); // Remove elements from editedVars/deletedVars that refer to variables that // don’t exist anymore. This can be caused by a realtime update // of `environmentVariables` (see CX-5439). const prevEnvironmentVariables = useRef<Array<T>>(); useEffect(() => { if ( !environmentVariables || prevEnvironmentVariables.current === environmentVariables ) return; prevEnvironmentVariables.current = environmentVariables; const oldEditedVars = formState.values.editedVars; const oldDeletedVars = formState.values.deletedVars; if (oldEditedVars.length === 0 && oldDeletedVars.length === 0) return; const existingEnvironmentVariables = new Set( environmentVariables.map(({ name }) => name), ); const newEditedVars = oldEditedVars.filter((v) => existingEnvironmentVariables.has(v.oldEnvVar.name), ); if (newEditedVars.length !== oldEditedVars.length) { void formState.setFieldValue("editedVars", newEditedVars); } const newDeletedVars = oldDeletedVars.filter((v) => existingEnvironmentVariables.has(v.name), ); if (newDeletedVars.length !== oldDeletedVars.length) { void formState.setFieldValue("deletedVars", newDeletedVars); } }, [environmentVariables, formState]); return ( <Form className="flex flex-col"> {environmentVariables === undefined ? ( <Spinner /> ) : ( <> <div className="divide-y divide-border-transparent"> {(environmentVariables.length > 0 || formState.values.newVars.length > 0) && ( <div className={classNames( ENVIRONMENT_VARIABLES_ROW_CLASSES, "hidden md:grid", )} > <div className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <span className="text-xs text-content-secondary">Name</span> </div> <div className="flex flex-col gap-1"> <span className="text-xs text-content-secondary">Value</span> </div> </div> )} {environmentVariables?.map((value) => ( <EnvironmentVariableListItem key={value.name} environmentVariable={value} hasAdminPermissions={hasAdminPermissions} /> ))} </div> <NewEnvVars existingEnvVars={environmentVariables} hasAdminPermissions={hasAdminPermissions} /> {environmentVariables.length >= MAX_NUMBER_OF_ENV_VARS && ( <div> <Callout variant="error"> You've reached the environment variable limit ( {MAX_NUMBER_OF_ENV_VARS}). Contact support@convex.dev if you need more. </Callout> </div> )} </> )} </Form> ); } // Adapted from https://github.com/motdotla/dotenv/blob/cf4c56957974efb7238ecaba6f16e0afa895c194/lib/main.js#L12 const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; function _parseEnvVars(input: string): Record<string, string> { const obj: Record<string, string> = {}; // Convert line breaks to same format const lines = input.replace(/\r\n?/gm, "\n"); let match = LINE.exec(lines); while (match !== null) { const key = match[1]; // Default undefined or null to empty string let value = match[2] || ""; // Remove whitespace value = value.trim(); // Check if double quoted const maybeQuote = value[0]; // Remove surrounding quotes value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); // Expand newlines if double quoted if (maybeQuote === '"') { value = value.replace(/\\n/g, "\n"); value = value.replace(/\\r/g, "\r"); } // Add to object obj[key] = value; match = LINE.exec(lines); } return obj; } export function parseEnvVars( input: string, ): Array<BaseEnvironmentVariable> | null { const parsedEnvVars = _parseEnvVars(input); // Ignore if the pasted string does not match an environment variable string if (Object.keys(parsedEnvVars).length === 0) { return null; } return Object.entries(parsedEnvVars).map(([name, value]) => ({ name, value, })); } function DisplayEnvVar<T extends BaseEnvironmentVariable>({ environmentVariable, onEdit, onDelete, hasAdminPermissions, }: { environmentVariable: T; onEdit: () => void; onDelete: () => void; hasAdminPermissions: boolean; }) { const formState = useFormikContext<FormState<T>>(); const [showValue, setShowValue] = useState(false); return ( <div className={ENVIRONMENT_VARIABLES_ROW_CLASSES}> <div className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <div className="flex h-[2.375rem] items-center truncate text-content-primary md:col-span-1"> {environmentVariable.name} </div> </div> <div className="flex flex-col gap-1"> <div className="flex h-[2.375rem] items-center gap-1 font-mono"> <Button tip={showValue ? "Hide" : "Show"} type="button" onClick={() => setShowValue(!showValue)} variant="neutral" inline size="sm" icon={showValue ? <EyeNoneIcon /> : <EyeOpenIcon />} /> {showValue ? ( <span className="truncate text-content-primary"> {environmentVariable.value} </span> ) : ( <span title="Hidden environment variable" className="text-content-primary" > •••••• </span> )} </div> </div> <div className="flex gap-2"> <Button tip={ !hasAdminPermissions ? "You do not have permission to edit environment variables." : "Edit" } type="button" onClick={() => onEdit()} variant="neutral" size="sm" inline icon={<Pencil1Icon />} disabled={formState.isSubmitting || !hasAdminPermissions} /> <Button tip="Copy Value" type="button" onClick={async () => { await copyTextToClipboard(environmentVariable.value); toast( "success", "Environment variable value copied to the clipboard.", ); }} variant="neutral" size="sm" inline icon={<ClipboardCopyIcon />} disabled={formState.isSubmitting} /> <Button tip={ !hasAdminPermissions ? "You do not have permission to delete environment variables." : "Delete" } type="button" onClick={() => onDelete()} variant="danger" size="sm" inline icon={<TrashIcon />} disabled={formState.isSubmitting || !hasAdminPermissions} /> </div> </div> ); } function DeletedEnvVar<T extends BaseEnvironmentVariable>({ environmentVariable, onCancelDelete, }: { environmentVariable: T; onCancelDelete: () => void; }) { const formState = useFormikContext<FormState<T>>(); return ( <div className={ENVIRONMENT_VARIABLES_ROW_CLASSES}> <div className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <div className="flex h-[2.375rem] items-center truncate text-content-primary md:col-span-1"> {environmentVariable.name} </div> </div> <div className="flex flex-col gap-1"> <div className="flex h-[2.375rem] items-center justify-center gap-1 bg-background-error"> <TrashIcon className="text-content-error" /> Will be deleted </div> </div> <div className="flex items-center justify-center"> <Button inline size="sm" onClick={() => onCancelDelete()} disabled={formState.isSubmitting} > Cancel </Button> </div> </div> ); } const EnvVarName = z .string() .min(1, "Environment variable name is required.") .max(40, "Environment variable name cannot exceed 40 characters.") .refine( (n) => /^[a-zA-Z_]+[a-zA-Z0-9_]*$/.test(n), "Name must start with a letter and only contain letters, numbers, and underscores.", ); const EnvVarValue = z .string() .min(1, "Environment variable value is required.") .max(8192, "Environment variable value cannot be larger than 8KB"); function EditEnvVarForm<T extends BaseEnvironmentVariable>({ editIndex, onCancelEdit, }: { editIndex: number; onCancelEdit: () => void; }) { const nameId = useId(); const valueId = useId(); const formState = useFormikContext<FormState<T>>(); const { value } = (formState.values as any).editedVars[editIndex].newEnvVar; return ( <div> <div className={ENVIRONMENT_VARIABLES_ROW_CLASSES}> <label htmlFor={nameId} className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <ValidatedTextInput formKey={`editedVars[${editIndex}].newEnvVar.name`} id={nameId} /> </label> <label htmlFor={valueId} className="flex grow flex-col flex-wrap gap-1"> <ValidatedTextInput formKey={`editedVars[${editIndex}].newEnvVar.value`} id={valueId} noAutocomplete /> </label> <div className="flex items-center justify-center"> <Button type="button" inline size="sm" onClick={() => onCancelEdit()} disabled={formState.isSubmitting} > Cancel </Button> </div> </div> {value.length > 1 && value.startsWith('"') && value.endsWith('"') && ( <Callout className="mb-2 w-full"> Environment variables usually shouldn't be surrounded by quotes. Quotes are useful in shell syntax and .env files but shouldn't be included in the environment variable value. </Callout> )} </div> ); } function EnvironmentVariableListItem< T extends { name: string; value: string }, >({ environmentVariable, hasAdminPermissions, }: { environmentVariable: T; hasAdminPermissions: boolean; }) { const formState = useFormikContext<FormState<T>>(); const editIndex = formState.values.editedVars.findIndex( (edit) => edit.oldEnvVar === environmentVariable, ); const isEditing = editIndex !== -1; if (isEditing) { return ( <EditEnvVarForm editIndex={editIndex} onCancelEdit={() => { const newEditedVars = [ ...formState.values.editedVars.slice(0, editIndex), ...formState.values.editedVars.slice(editIndex + 1), ]; void formState.setFieldValue("editedVars", newEditedVars); // https://github.com/jaredpalmer/formik/issues/2059#issuecomment-612733378 setTimeout(() => formState.setTouched({ editedVars: newEditedVars.map(() => ({ newEnvVar: { name: true, value: true }, })), }), ); }} /> ); } const deleteIndex = formState.values.deletedVars.findIndex( (deletedVar) => deletedVar === environmentVariable, ); const isDeleting = deleteIndex !== -1; if (isDeleting) { return ( <DeletedEnvVar environmentVariable={environmentVariable} onCancelDelete={() => { const newDeletedVars = [ ...formState.values.deletedVars.slice(0, deleteIndex), ...formState.values.deletedVars.slice(deleteIndex + 1), ]; void formState.setFieldValue("deletedVars", newDeletedVars); }} /> ); } return ( <DisplayEnvVar hasAdminPermissions={hasAdminPermissions} environmentVariable={environmentVariable} onEdit={() => { void formState.setFieldValue("editedVars", [ ...formState.values.editedVars, { oldEnvVar: environmentVariable, newEnvVar: { name: environmentVariable.name, value: environmentVariable.value, }, }, ]); }} onDelete={() => { void formState.setFieldValue("deletedVars", [ ...formState.values.deletedVars, environmentVariable, ]); }} /> ); } function NewEnvVars<T extends BaseEnvironmentVariable>({ existingEnvVars, hasAdminPermissions, }: { existingEnvVars: Array<T>; hasAdminPermissions: boolean; }) { const formState = useFormikContext<FormState<T>>(); const handlePaste = (envVars: Array<BaseEnvironmentVariable>) => { const newVars = formState.values.newVars.filter( ({ name, value }) => name !== "" || value !== "", ); let totalEnvVars = existingEnvVars.length; let tooManyEnvVars = false; envVars.forEach((envVar) => { if (totalEnvVars < MAX_NUMBER_OF_ENV_VARS) { newVars.push(envVar); totalEnvVars += 1; } else { tooManyEnvVars = true; } }); void formState.setFieldValue("newVars", newVars, true); void formState.setFieldValue("tooManyEnvVars", tooManyEnvVars); // https://github.com/jaredpalmer/formik/issues/2059#issuecomment-612733378 setTimeout(() => formState.setTouched({ newVars: newVars.map(() => ({ name: true, value: true })), }), ); }; return ( <div> {formState.values.newVars.length > 0 && ( <> <div className="divide-y divide-border-transparent border-t"> {formState.values.newVars.map((_, index) => ( <NewEnvVar key={index} newVarIndex={index} onDelete={() => { const newVars = [ ...formState.values.newVars.slice(0, index), ...formState.values.newVars.slice(index + 1), ]; void formState.setFieldValue("newVars", newVars); void formState.setFieldValue("tooManyEnvVars", false); // https://github.com/jaredpalmer/formik/issues/2059#issuecomment-612733378 setTimeout(() => { void formState.setTouched({ newVars: newVars.map(() => ({ name: true, value: true })), }); }); }} onPasteVariables={(input) => handlePaste(input)} isLastVariable={index === formState.values.newVars.length - 1} /> ))} </div> <p className="pt-2 pb-4 text-content-secondary"> Tip: Paste your .env file directly into here! </p> </> )} <div className="my-2 flex place-content-between"> <div className="flex gap-2"> {existingEnvVars.length + formState.values.newVars.length < MAX_NUMBER_OF_ENV_VARS && ( <Button type="button" variant="neutral" onClick={() => { void formState.setFieldValue("newVars", [ ...formState.values.newVars, { name: "", value: "", }, ]); }} icon={<PlusIcon />} disabled={!hasAdminPermissions} tip={ !hasAdminPermissions ? "You do not have permission to add new environment variables." : undefined } > Add </Button> )} {existingEnvVars.length > 0 && ( <Button type="button" variant="neutral" inline onClick={async () => { await copyTextToClipboard( existingEnvVars .map(({ name, value }) => `${name}=${value}`) .join("\n"), ); toast( "success", "Environment variables copied to the clipboard.", ); }} icon={<ClipboardCopyIcon />} > Copy All </Button> )} </div> {(formState.values.editedVars.length > 0 || formState.values.newVars.length > 0 || formState.values.deletedVars.length > 0) && ( <Button type="submit" disabled={formState.isSubmitting || !formState.isValid} > {formState.values.editedVars.length + formState.values.newVars.length + formState.values.deletedVars.length >= 2 ? "Save All" : "Save"} </Button> )} </div> {formState.values.tooManyEnvVars && ( <Callout variant="error"> You've reached the environment variable limit ( {MAX_NUMBER_OF_ENV_VARS}). Some pasted environment variables have been omitted. </Callout> )} </div> ); } function NewEnvVar({ newVarIndex, onDelete, onPasteVariables, isLastVariable, }: { newVarIndex: number; onDelete: () => void; onPasteVariables: (variables: Array<BaseEnvironmentVariable>) => void; isLastVariable: boolean; }) { const nameId = useId(); const valueId = useId(); const formState = useFormikContext(); const { value } = (formState.values as any).newVars[newVarIndex]; return ( <div className={ENVIRONMENT_VARIABLES_ROW_CLASSES}> <label htmlFor={nameId} className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <div> <ValidatedTextInput formKey={`newVars[${newVarIndex}].name`} id={nameId} onPaste={(e) => { const variables = parseEnvVars(e.clipboardData.getData("text")); if (variables) { e.preventDefault(); onPasteVariables(variables); } }} autoFocus={isLastVariable} /> </div> </label> <label htmlFor={valueId} className="flex grow flex-col gap-1"> <div> <ValidatedTextInput formKey={`newVars[${newVarIndex}].value`} id={valueId} noAutocomplete /> {value.length > 1 && value.startsWith('"') && value.endsWith('"') && ( <Callout> Environment variables usually shouldn't be surrounded by quotes. Quotes are useful in shell syntax and .env files but shouldn't be included in the environment variable value. </Callout> )} </div> </label> <div className="pt-1"> <Button tip="Remove" type="button" onClick={() => { onDelete(); }} variant="neutral" inline size="sm" icon={<Cross2Icon />} /> </div> </div> ); } function ValidatedTextInput({ formKey, id, noAutocomplete = false, onPaste = undefined, autoFocus = false, }: { formKey: string; id: string; noAutocomplete?: boolean; onPaste?: ClipboardEventHandler; autoFocus?: boolean; }) { const formState = useFormikContext(); const error = (formState.errors as Record<string, string>)[formKey]; return ( <TextInput id={id} labelHidden disabled={formState.isSubmitting} {...formState.getFieldProps(formKey)} autoComplete={noAutocomplete ? "off" : undefined} onPaste={onPaste} error={ (getIn(formState.touched, formKey) || error === ERROR_ENV_VAR_NOT_UNIQUE) && error } autoFocus={autoFocus} /> ); }

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/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server