Skip to main content
Glama

Convex MCP server

Official
by get-convex
CustomDomains.tsx20.3 kB
import { CheckCircledIcon, ExclamationTriangleIcon, PlusIcon, TrashIcon, } from "@radix-ui/react-icons"; import classNames from "classnames"; import { Button } from "@ui/Button"; import { Tooltip } from "@ui/Tooltip"; import { Combobox, Option } from "@ui/Combobox"; import { LocalDevCallout } from "@common/elements/LocalDevCallout"; import { Callout } from "@ui/Callout"; import { captureMessage } from "@sentry/nextjs"; import { ENVIRONMENT_VARIABLES_ROW_CLASSES, ENVIRONMENT_VARIABLE_NAME_COLUMN, } from "@common/features/settings/components/EnvironmentVariables"; import { Sheet } from "@ui/Sheet"; import { ConfirmationDialog } from "@ui/ConfirmationDialog"; import { TextInput } from "@ui/TextInput"; import { useFormik } from "formik"; import { useDeployments } from "api/deployments"; import { useCurrentProject } from "api/projects"; import { useHasProjectAdminPermissions } from "api/roles"; import Link from "next/link"; import { useState, useMemo } from "react"; import { Team, VanityDomainRequestArgs, VanityDomainResponse, } from "generatedApi"; import { useListVanityDomains, useCreateVanityDomain, useDeleteVanityDomain, } from "api/vanityDomains"; import { useDeploymentUrl } from "@common/lib/deploymentApi"; import { DeploymentInfoProvider } from "providers/DeploymentInfoProvider"; import { MaybeDeploymentApiProvider } from "providers/MaybeDeploymentApiProvider"; import { WaitForDeploymentApi } from "@common/lib/deploymentContext"; import { useQuery } from "convex/react"; import udfs from "@common/udfs"; import { useUpdateCanonicalUrl } from "hooks/deploymentApi"; import { Loading } from "@ui/Loading"; export function CustomDomains({ team, hasEntitlement, }: { team: Team; hasEntitlement: boolean; }) { const project = useCurrentProject(); const deployment = useDeployments(project?.id).deployments?.find( (d) => d.deploymentType === "prod", ); const hasAdminPermissions = useHasProjectAdminPermissions(project?.id); const vanityDomains = useListVanityDomains( hasEntitlement ? deployment?.name : undefined, ); const hasEditAccess = hasEntitlement && hasAdminPermissions; return ( <Sheet> <div className="flex flex-col gap-4"> <div> <h3 className="mb-2">Custom Domains</h3> <p className="max-w-prose"> Add a custom domain to your Production Convex deployment. Domains for the Convex API (your queries, mutations, and actions) and your HTTP actions are configured separately. </p> </div> <div> {!hasEntitlement && ( <> <Callout> <div> Custom domains are{" "} <span className="font-semibold"> only available on the Pro plan </span> .{" "} <Link href={`/${team?.slug}/settings/billing`} className="underline" > Upgrade to get access. </Link> </div> </Callout> <LocalDevCallout className="flex-col" tipText="Tip: Run this to enable custom domains locally:" command={`cargo run --bin big-brain-tool -- --dev grant-entitlement --team-entitlement custom_domains_enabled --team-id ${team?.id} --reason "local" true --for-real`} /> </> )} {deployment ? ( <VanityDomainForm disabled={!hasEditAccess} deploymentName={deployment.name} /> ) : ( <span className="text-content-secondary"> This project does not have a Production deployment yet. </span> )} {vanityDomains && vanityDomains.length > 0 && ( <> <div className={classNames( "hidden md:grid", ENVIRONMENT_VARIABLES_ROW_CLASSES, )} > <div className={`flex flex-col gap-1 ${ENVIRONMENT_VARIABLE_NAME_COLUMN}`} > <span className="text-xs text-content-secondary">Domain</span> </div> <div className="flex flex-col gap-1"> <span className="text-xs text-content-secondary"> Request Destination{" "} </span> </div> </div> <div className="divide-y divide-border-transparent border-t"> {vanityDomains .sort((a, b) => a.creationTime - b.creationTime) .reverse() .map((domain, index) => ( <DisplayVanityDomain key={index} vanityDomain={domain} enabled={hasEditAccess} /> ))} </div> </> )} {deployment && ( <div className="border-t"> <ProdProvider deploymentName={deployment.name}> <CanonicalDomainForm deploymentName={deployment.name} vanityDomains={vanityDomains} /> </ProdProvider> </div> )} </div> </div> </Sheet> ); } export function ProdProvider({ children, deploymentName, }: { children: React.ReactNode; deploymentName: string; }) { return ( <DeploymentInfoProvider deploymentOverride={deploymentName}> <MaybeDeploymentApiProvider deploymentOverride={deploymentName}> <WaitForDeploymentApi sizeClass="hidden"> {children} </WaitForDeploymentApi> </MaybeDeploymentApiProvider> </DeploymentInfoProvider> ); } function CanonicalDomainForm({ deploymentName, vanityDomains, }: { deploymentName: string; vanityDomains?: VanityDomainResponse[]; }) { const deploymentUrl = useDeploymentUrl(); const canonicalCloudUrl = useQuery(udfs.convexCloudUrl.default); const canonicalSiteUrl = useQuery(udfs.convexSiteUrl.default); return ( <div className="flex flex-col gap-3"> <h4 className="mt-3">Override Production Environment Variables</h4> <p className="max-w-prose"> Replace your production environment variables everywhere in your app. All internal references to your <code>CONVEX_SITE_URL</code> and{" "} <code>CONVEX_CLOUD_URL</code> will change to the url you set. This can affect WebSocket and HTTP clients, storage urls, and Convex Auth. </p> <CanonicalUrlCombobox label={ <span className="flex items-center gap-2"> <code>process.env.CONVEX_CLOUD_URL</code> </span> } defaultUrl={{ kind: "default", url: deploymentUrl }} canonicalUrl={ canonicalCloudUrl === undefined ? { kind: "loading" } : { kind: "loaded", url: canonicalCloudUrl } } vanityDomains={vanityDomains} requestDestination="convexCloud" /> <CanonicalUrlCombobox label={ <span className="flex flex-row items-center gap-1"> <code>process.env.CONVEX_SITE_URL</code> </span> } defaultUrl={ deploymentUrl === `https://${deploymentName}.convex.cloud` ? { kind: "default", url: `https://${deploymentName}.convex.site` } : { kind: "unknownDefault" } } canonicalUrl={ canonicalSiteUrl === undefined ? { kind: "loading" } : { kind: "loaded", url: canonicalSiteUrl } } vanityDomains={vanityDomains} requestDestination="convexSite" /> </div> ); } type DefaultUrlOption = | { // The default url for this deployment, before any overrides were applied. kind: "default"; url: string; } | { // In this case, we don't know what the default url should be. // In some cases the deployment doesn't expose its default url through an API. kind: "unknownDefault"; }; type UrlOption = | { // Either a default url or a canonical url that has yet to be loaded // from the deployment. This should be a transient state. kind: "loading"; } | DefaultUrlOption | { // The current canonical url for the deployment, which doesn't match the // default or any custom domains. // If you switch away from this URL, you probably won't be able to switch // back to it. kind: "disconnectedCanonical"; url: string; } | { kind: "custom"; url: string; }; type CanonicalUrl = | { kind: "loading"; } | { kind: "loaded"; url: string; }; export function CanonicalUrlCombobox({ label, defaultUrl, canonicalUrl, vanityDomains, requestDestination, }: { label: React.ReactNode; defaultUrl: DefaultUrlOption; canonicalUrl: CanonicalUrl; vanityDomains?: VanityDomainResponse[]; requestDestination: "convexCloud" | "convexSite"; }) { const vanityDomainsForRequestDestination = useMemo( () => vanityDomains?.filter( (v) => v.requestDestination === requestDestination, ) || [], [vanityDomains, requestDestination], ); const canonicalIsKnownDefault = canonicalUrl.kind === "loaded" && defaultUrl.kind === "default" && canonicalUrl.url === defaultUrl.url; const canonicalIsCustom = canonicalUrl.kind === "loaded" && vanityDomainsForRequestDestination.some( (v) => `https://${v.domain}` === canonicalUrl.url, ); // If we don't know what the default should be, and we don't recognize the canonical URL, // assume that the canonical URL is the default. const canonicalIsUnknownDefault = canonicalUrl.kind === "loaded" && defaultUrl.kind === "unknownDefault" && !canonicalIsCustom; const canonicalIsDefault = canonicalIsKnownDefault || canonicalIsUnknownDefault; const effectiveDefaultUrl: UrlOption = useMemo(() => { if (canonicalIsUnknownDefault) { return { kind: "default", url: canonicalUrl.url }; } return defaultUrl; }, [canonicalIsUnknownDefault, canonicalUrl, defaultUrl]); const canonicalUrlOption: UrlOption = useMemo(() => { if (canonicalUrl.kind === "loading") { return { kind: "loading" }; } if (canonicalIsCustom) { return { kind: "custom", url: canonicalUrl.url }; } if (canonicalIsDefault) { return { kind: "default", url: canonicalUrl.url }; } return { kind: "disconnectedCanonical", url: canonicalUrl.url }; }, [canonicalIsCustom, canonicalIsDefault, canonicalUrl]); const updateCanonicalUrl = useUpdateCanonicalUrl(requestDestination); const options: Option<UrlOption>[] = useMemo( () => [ { label: effectiveDefaultUrl.kind === "default" ? `${effectiveDefaultUrl.url} (default)` : "default", value: effectiveDefaultUrl, }, ...(canonicalUrlOption.kind === "disconnectedCanonical" ? [ { label: `${canonicalUrlOption.url} (disconnected)`, value: canonicalUrlOption, }, ] : []), ...vanityDomainsForRequestDestination.map((v) => ({ label: `https://${v.domain}${ v.verificationTime ? "" : " (unverified)" }`, value: { kind: "custom" as const, url: `https://${v.domain}` }, })), ], [ effectiveDefaultUrl, canonicalUrlOption, vanityDomainsForRequestDestination, ], ); const disabled = options.length <= 1; if (canonicalUrlOption.kind === "loading") { return <Loading className="h-8 w-full" fullHeight={false} />; } return ( <div className="flex flex-col gap-1"> <Combobox label={label} labelHidden={false} disabled={disabled} buttonProps={{ tip: disabled ? `Add a custom domain first` : undefined, }} buttonClasses="w-fit" optionsWidth="fit" options={options} selectedOption={canonicalUrlOption} setSelectedOption={async (value: UrlOption | null) => { if (value === null || value.kind === "loading") { captureMessage( "Unexpected value selected in CanonicalUrlCombobox", "error", ); return; } await updateCanonicalUrl( value.kind === "default" || value.kind === "unknownDefault" ? null : value.url, ); }} disableSearch /> </div> ); } function VanityDomainForm({ deploymentName, disabled, }: { deploymentName: string; disabled?: boolean; }) { const createVanityDomain = useCreateVanityDomain(deploymentName); const formState = useFormik<VanityDomainRequestArgs>({ validateOnChange: true, initialValues: { domain: "", requestDestination: "convexSite", }, validate: (values: { domain?: string }) => { const errors: Partial<VanityDomainRequestArgs> = {}; if ( !values.domain || values.domain === "" || !values.domain.includes(".") ) { errors.domain = "Enter a valid domain name"; } return errors; }, onSubmit: async (values: VanityDomainRequestArgs) => { await createVanityDomain({ domain: values.domain, requestDestination: values.requestDestination, }); formState.resetForm(); }, }); return ( <form className="mb-4 flex flex-col gap-2 overflow-x-clip pt-2 md:flex-row" onSubmit={(e) => { e.preventDefault(); formState.handleSubmit(); }} > <VanityDomainInputs formState={formState} disabled={disabled} deploymentName={deploymentName} /> <Button className="flex w-fit" type="submit" disabled={ disabled || formState.isSubmitting || !formState.isValid || formState.values.domain === "" } tip={ disabled ? "You do not have permission to add custom domains" : undefined } icon={<PlusIcon />} variant="primary" > Add Domain </Button> </form> ); } export function VanityDomainInputs({ formState, deploymentName, disabled = false, }: { formState: ReturnType<typeof useFormik<VanityDomainRequestArgs>>; deploymentName: string; disabled?: boolean; }) { return ( <div className="flex w-full flex-col gap-2 md:flex-row"> <div className="flex grow flex-col gap-1"> <TextInput placeholder="Custom domain URL" error={formState.errors.domain} onChange={formState.handleChange} value={formState.values.domain} id="domain" labelHidden disabled={disabled} /> </div> <Combobox buttonClasses="w-fit" optionsWidth="full" label="Request Destination" options={[ { label: `HTTP Actions (${deploymentName}.convex.site)`, value: "convexSite", }, { label: `Convex API (${deploymentName}.convex.cloud)`, value: "convexCloud", }, ]} selectedOption={formState.values.requestDestination} setSelectedOption={async ( value: "convexSite" | "convexCloud" | null, ) => { if (value === null) { return; } await formState.setFieldValue("requestDestination", value); }} disableSearch disabled={disabled} /> </div> ); } function DisplayVanityDomain({ vanityDomain, enabled, }: { vanityDomain: VanityDomainResponse; enabled: boolean; }) { const [showDeleteModal, setShowDeleteModal] = useState(false); return ( <div className="flex flex-col"> <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"> {vanityDomain.domain} {vanityDomain.verificationTime && ( <Tooltip tip="This domain is verified and can receive traffic." side="right" > <CheckCircledIcon className="m-3 text-green-700 dark:text-green-200" /> </Tooltip> )} </div> </div> <div className="flex h-[2.375rem] items-center gap-1"> <span className="truncate text-content-primary"> {vanityDomain.requestDestination === "convexCloud" ? "Convex API" : "HTTP Actions"} </span> </div> <div className="flex h-full items-center justify-end gap-2"> <Button tip={ !enabled ? "You do not have permission to delete custom domains." : "Delete" } type="button" onClick={() => setShowDeleteModal(true)} variant="danger" size="sm" inline icon={<TrashIcon />} disabled={!enabled} /> {showDeleteModal && ( <DeleteDomainModal onClose={() => setShowDeleteModal(false)} domain={vanityDomain} /> )} </div> </div> {!vanityDomain.verificationTs && ( <> <Callout className="mt-0 mb-4 w-72 gap-2 align-middle"> <div className="ml-1 flex w-full gap-3"> <ExclamationTriangleIcon className="mt-1" /> <span className="font-semibold"> This domain is not verified yet. </span> </div> </Callout> <span className="mb-4 font-semibold"> Set the following records on your DNS provider: </span> <div className="rounded-sm border p-2"> <div className="grid grid-cols-1 p-2 md:grid md:grid-cols-[2fr_6fr_3fr] md:gap-2"> {/* Header */} {["Type", "Name", "Value"].map((header) => ( <div className="hidden font-semibold text-content-secondary md:block"> {header} </div> ))} {/* Records */} <code className="truncate font-bold break-words md:font-normal"> CNAME </code> <code className="truncate break-words"> {vanityDomain.domain} </code> <code className="truncate break-words">convex.domains</code> <code className="truncate font-bold break-words md:font-normal"> TXT </code> <code className="truncate break-words"> _convex_domains.{vanityDomain.domain} </code> <code className="truncate break-words"> {vanityDomain.instanceName} </code> </div> </div> <span className="my-4 font-light"> It may take up to 30 minutes to verify your domain and start serving traffic. </span> </> )} </div> ); } function DeleteDomainModal({ domain, onClose, }: { domain: VanityDomainResponse; onClose: () => void; }) { const deleteVanityDomain = useDeleteVanityDomain(domain.instanceName); const handleDelete = async () => { await deleteVanityDomain({ domain: domain.domain, requestDestination: domain.requestDestination, }); }; return ( <ConfirmationDialog onClose={onClose} onConfirm={handleDelete} confirmText="Delete" dialogTitle="Delete Custom Domain" variant="danger" dialogBody={ <> Are you sure you want to delete the custom domain{" "} <span className="font-semibold">{domain.domain}</span>? </> } /> ); }

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