Skip to main content
Glama

Convex MCP server

Official
by get-convex
config.ts35.2 kB
import chalk from "chalk"; import equal from "deep-equal"; import { EOL } from "os"; import path from "path"; import { Context } from "../../bundler/context.js"; import { changeSpinner, logError, logFailure, logFinishedStep, logMessage, showSpinner, } from "../../bundler/log.js"; import { Bundle, BundleHash, bundle, bundleAuthConfig, entryPointsByEnvironment, } from "../../bundler/index.js"; import { version } from "../version.js"; import { deploymentDashboardUrlPage } from "./dashboard.js"; import { formatSize, functionsDir, ErrorData, loadPackageJson, deploymentFetch, deprecationCheckWarning, logAndHandleFetchError, ThrowingFetchError, currentPackageHomepage, } from "./utils/utils.js"; import { createHash } from "crypto"; import { promisify } from "util"; import zlib from "zlib"; import { recursivelyDelete } from "./fsUtils.js"; import { NodeDependency } from "./deployApi/modules.js"; import { ComponentDefinitionPath } from "./components/definition/directoryStructure.js"; import { LocalDeploymentError, printLocalDeploymentOnError, } from "./localDeployment/errors.js"; import { debugIsolateBundlesSerially } from "../../bundler/debugBundle.js"; import { ensureWorkosEnvironmentProvisioned } from "./workos/workos.js"; export { productionProvisionHost, provisionHost } from "./utils/utils.js"; const brotli = promisify(zlib.brotliCompress); /** Type representing auth configuration. */ export interface AuthInfo { // Provider-specific application identifier. Corresponds to the `aud` field in an OIDC token. applicationID: string; // Domain used for authentication. Corresponds to the `iss` field in an OIDC token. domain: string; } /** Type representing Convex project configuration. */ export interface ProjectConfig { functions: string; node: { externalPackages: string[]; nodeVersion?: string; }; generateCommonJSApi: boolean; // deprecated project?: string | undefined; // deprecated team?: string | undefined; // deprecated prodUrl?: string | undefined; // deprecated authInfo?: AuthInfo[]; // These are beta flags for using static codegen from the `api.d.ts` and `dataModel.d.ts` files. codegen: { staticApi: boolean; staticDataModel: boolean; }; } export interface Config { projectConfig: ProjectConfig; modules: Bundle[]; nodeDependencies: NodeDependency[]; schemaId?: string; udfServerVersion?: string; nodeVersion?: string | undefined; } export interface ConfigWithModuleHashes { projectConfig: ProjectConfig; moduleHashes: BundleHash[]; nodeDependencies: NodeDependency[]; schemaId?: string; udfServerVersion?: string; } const DEFAULT_FUNCTIONS_PATH = "convex/"; /** Check if object is of AuthInfo type. */ function isAuthInfo(object: any): object is AuthInfo { return ( "applicationID" in object && typeof object.applicationID === "string" && "domain" in object && typeof object.domain === "string" ); } function isAuthInfos(object: any): object is AuthInfo[] { return Array.isArray(object) && object.every((item: any) => isAuthInfo(item)); } /** Error parsing ProjectConfig representation. */ class ParseError extends Error {} /** Parse object to ProjectConfig. */ export async function parseProjectConfig( ctx: Context, obj: any, ): Promise<ProjectConfig> { if (typeof obj !== "object") { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `convex.json` to contain an object", }); } if (typeof obj.node === "undefined") { obj.node = { externalPackages: [], }; } else { if (typeof obj.node.externalPackages === "undefined") { obj.node.externalPackages = []; } else if ( !Array.isArray(obj.node.externalPackages) || !obj.node.externalPackages.every((item: any) => typeof item === "string") ) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `node.externalPackages` in `convex.json` to be an array of strings", }); } if ( typeof obj.node.nodeVersion !== "undefined" && typeof obj.node.nodeVersion !== "string" ) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `node.nodeVersion` in `convex.json` to be a string", }); } } if (typeof obj.generateCommonJSApi === "undefined") { obj.generateCommonJSApi = false; } else if (typeof obj.generateCommonJSApi !== "boolean") { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `generateCommonJSApi` in `convex.json` to be true or false", }); } if (typeof obj.functions === "undefined") { obj.functions = DEFAULT_FUNCTIONS_PATH; } else if (typeof obj.functions !== "string") { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `functions` in `convex.json` to be a string", }); } // Allow the `authInfo` key to be omitted, treating it as an empty list of providers. if (obj.authInfo !== undefined) { if (!isAuthInfos(obj.authInfo)) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `authInfo` in `convex.json` to be type AuthInfo[]", }); } } if (typeof obj.codegen === "undefined") { obj.codegen = {}; } if (typeof obj.codegen !== "object") { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `codegen` in `convex.json` to be an object", }); } if (typeof obj.codegen.staticApi === "undefined") { obj.codegen.staticApi = false; } if (typeof obj.codegen.staticDataModel === "undefined") { obj.codegen.staticDataModel = false; } if ( typeof obj.codegen.staticApi !== "boolean" || typeof obj.codegen.staticDataModel !== "boolean" ) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `codegen.staticApi` and `codegen.staticDataModel` in `convex.json` to be booleans", }); } return obj; } // Parse a deployment config returned by the backend, picking out // the fields we care about. function parseBackendConfig(obj: any): { functions: string; authInfo?: AuthInfo[]; nodeVersion?: string; } { function throwParseError(message: string) { // Unexpected error // eslint-disable-next-line no-restricted-syntax throw new ParseError(message); } if (typeof obj !== "object") { throwParseError("Expected an object"); } const { functions, authInfo, nodeVersion } = obj; if (typeof functions !== "string") { throwParseError("Expected functions to be a string"); } // Allow the `authInfo` key to be omitted if ((authInfo ?? null) !== null && !isAuthInfos(authInfo)) { throwParseError("Expected authInfo to be type AuthInfo[]"); } if (typeof nodeVersion !== "undefined" && typeof nodeVersion !== "string") { throwParseError("Expected nodeVersion to be a string"); } return { functions, ...((authInfo ?? null) !== null ? { authInfo: authInfo } : {}), ...((nodeVersion ?? null) !== null ? { nodeVersion: nodeVersion } : {}), }; } export function configName(): string { return "convex.json"; } export async function configFilepath(ctx: Context): Promise<string> { const configFn = configName(); // We used to allow src/convex.json, but no longer (as of 10/7/2022). // Leave an error message around to help people out. We can remove this // error message after a couple months. const preferredLocation = configFn; const wrongLocation = path.join("src", configFn); // Allow either location, but not both. const preferredLocationExists = ctx.fs.exists(preferredLocation); const wrongLocationExists = ctx.fs.exists(wrongLocation); if (preferredLocationExists && wrongLocationExists) { const message = `${chalk.red(`Error: both ${preferredLocation} and ${wrongLocation} files exist!`)}\nConsolidate these and remove ${wrongLocation}.`; return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: message, }); } if (!preferredLocationExists && wrongLocationExists) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `Error: Please move ${wrongLocation} to the root of your project`, }); } return preferredLocation; } export async function getFunctionsDirectoryPath(ctx: Context): Promise<string> { const { projectConfig, configPath } = await readProjectConfig(ctx); return functionsDir(configPath, projectConfig); } /** Read configuration from a local `convex.json` file. */ export async function readProjectConfig(ctx: Context): Promise<{ projectConfig: ProjectConfig; configPath: string; }> { if (!ctx.fs.exists("convex.json")) { // create-react-app bans imports from outside of src, so we can just // put the functions directory inside of src/ to work around this issue. const packages = await loadPackageJson(ctx); const isCreateReactApp = "react-scripts" in packages; return { projectConfig: { functions: isCreateReactApp ? `src/${DEFAULT_FUNCTIONS_PATH}` : DEFAULT_FUNCTIONS_PATH, node: { externalPackages: [], }, generateCommonJSApi: false, codegen: { staticApi: false, staticDataModel: false, }, }, configPath: configName(), }; } let projectConfig; const configPath = await configFilepath(ctx); try { projectConfig = await parseProjectConfig( ctx, JSON.parse(ctx.fs.readUtf8File(configPath)), ); } catch (err) { if (err instanceof ParseError || err instanceof SyntaxError) { logError(chalk.red(`Error: Parsing "${configPath}" failed`)); logMessage(chalk.gray(err.toString())); } else { logFailure( `Error: Unable to read project config file "${configPath}"\n` + " Are you running this command from the root directory of a Convex project? If so, run `npx convex dev` first.", ); if (err instanceof Error) { logError(chalk.red(err.message)); } } return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", errForSentry: err, // TODO -- move the logging above in here printedMessage: null, }); } return { projectConfig, configPath, }; } export async function enforceDeprecatedConfigField( ctx: Context, config: ProjectConfig, field: "team" | "project" | "prodUrl", ): Promise<string> { const value = config[field]; if (typeof value === "string") { return value; } const err = new ParseError(`Expected ${field} to be a string`); return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", errForSentry: err, printedMessage: `Error: Parsing convex.json failed:\n${chalk.gray(err.toString())}`, }); } /** * Given a {@link ProjectConfig}, add in the bundled modules to produce the * complete config. */ export async function configFromProjectConfig( ctx: Context, projectConfig: ProjectConfig, configPath: string, verbose: boolean, ): Promise<{ config: Config; bundledModuleInfos: BundledModuleInfo[]; }> { const baseDir = functionsDir(configPath, projectConfig); // We bundle Node.js and Convex JS runtime functions entry points separately // since they execute on different platforms. const entryPoints = await entryPointsByEnvironment(ctx, baseDir); // es-build prints errors to console which would clobber our spinner. if (verbose) { showSpinner("Bundling modules for Convex's runtime..."); } const convexResult = await bundle( ctx, baseDir, entryPoints.isolate, true, "browser", ); if (verbose) { logMessage( "Convex's runtime modules: ", convexResult.modules.map((m) => m.path), ); } // Bundle node modules. if (verbose && entryPoints.node.length !== 0) { showSpinner("Bundling modules for Node.js runtime..."); } const nodeResult = await bundle( ctx, baseDir, entryPoints.node, true, "node", path.join("_deps", "node"), projectConfig.node.externalPackages, ); if (verbose && entryPoints.node.length !== 0) { logMessage( "Node.js runtime modules: ", nodeResult.modules.map((m) => m.path), ); if (projectConfig.node.externalPackages.length > 0) { logMessage( "Node.js runtime external dependencies (to be installed on the server): ", [...nodeResult.externalDependencies.entries()].map( (a) => `${a[0]}: ${a[1]}`, ), ); } } const modules = convexResult.modules; modules.push(...nodeResult.modules); modules.push(...(await bundleAuthConfig(ctx, baseDir))); const nodeDependencies: NodeDependency[] = []; for (const [moduleName, moduleVersion] of nodeResult.externalDependencies) { nodeDependencies.push({ name: moduleName, version: moduleVersion }); } const bundledModuleInfos: BundledModuleInfo[] = Array.from( convexResult.bundledModuleNames.keys(), ).map((moduleName) => { return { name: moduleName, platform: "convex", }; }); bundledModuleInfos.push( ...Array.from(nodeResult.bundledModuleNames.keys()).map( (moduleName): BundledModuleInfo => { return { name: moduleName, platform: "node", }; }, ), ); return { config: { projectConfig: projectConfig, modules: modules, nodeDependencies: nodeDependencies, // We're just using the version this CLI is running with for now. // This could be different than the version of `convex` the app runs with // if the CLI is installed globally. udfServerVersion: version, nodeVersion: projectConfig.node.nodeVersion, }, bundledModuleInfos, }; } /** * Bundle modules one by one for good bundler errors. */ export async function debugIsolateEndpointBundles( ctx: Context, projectConfig: ProjectConfig, configPath: string, ): Promise<void> { const baseDir = functionsDir(configPath, projectConfig); const entryPoints = await entryPointsByEnvironment(ctx, baseDir); if (entryPoints.isolate.length === 0) { logFinishedStep("No non-'use node' modules found."); } await debugIsolateBundlesSerially(ctx, { entryPoints: entryPoints.isolate, extraConditions: [], dir: baseDir, }); } /** * Read the config from `convex.json` and bundle all the modules. */ export async function readConfig( ctx: Context, verbose: boolean, ): Promise<{ config: Config; configPath: string; bundledModuleInfos: BundledModuleInfo[]; }> { const { projectConfig, configPath } = await readProjectConfig(ctx); const { config, bundledModuleInfos } = await configFromProjectConfig( ctx, projectConfig, configPath, verbose, ); return { config, configPath, bundledModuleInfos }; } export async function upgradeOldAuthInfoToAuthConfig( ctx: Context, config: ProjectConfig, functionsPath: string, ) { if (config.authInfo !== undefined) { const authConfigPathJS = path.resolve(functionsPath, "auth.config.js"); const authConfigPathTS = path.resolve(functionsPath, "auth.config.js"); const authConfigPath = ctx.fs.exists(authConfigPathJS) ? authConfigPathJS : authConfigPathTS; const authConfigRelativePath = path.join( config.functions, ctx.fs.exists(authConfigPathJS) ? "auth.config.js" : "auth.config.ts", ); if (ctx.fs.exists(authConfigPath)) { await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `Cannot set auth config in both \`${authConfigRelativePath}\` and convex.json,` + ` remove it from convex.json`, }); } if (config.authInfo.length > 0) { const providersStringLines = JSON.stringify( config.authInfo, null, 2, ).split(EOL); const indentedProvidersString = [providersStringLines[0]] .concat(providersStringLines.slice(1).map((line) => ` ${line}`)) .join(EOL); ctx.fs.writeUtf8File( authConfigPath, `\ export default { providers: ${indentedProvidersString}, };`, ); logMessage( chalk.yellowBright( `Moved auth config from config.json to \`${authConfigRelativePath}\``, ), ); } delete config.authInfo; } return config; } /** Write the config to `convex.json` in the current working directory. */ export async function writeProjectConfig( ctx: Context, projectConfig: ProjectConfig, { deleteIfAllDefault }: { deleteIfAllDefault: boolean } = { deleteIfAllDefault: false, }, ) { const configPath = await configFilepath(ctx); const strippedConfig = filterWriteableConfig(stripDefaults(projectConfig)); if (Object.keys(strippedConfig).length > 0) { try { const contents = JSON.stringify(strippedConfig, undefined, 2) + "\n"; ctx.fs.writeUtf8File(configPath, contents, 0o644); } catch (err) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", errForSentry: err, printedMessage: `Error: Unable to write project config file "${configPath}" in current directory\n` + " Are you running this command from the root directory of a Convex project?", }); } } else if (deleteIfAllDefault && ctx.fs.exists(configPath)) { ctx.fs.unlink(configPath); logMessage( chalk.yellowBright( `Deleted ${configPath} since it completely matched defaults`, ), ); } ctx.fs.mkdir(functionsDir(configPath, projectConfig), { allowExisting: true, }); } function stripDefaults(projectConfig: ProjectConfig): any { const stripped: any = { ...projectConfig }; if (stripped.functions === DEFAULT_FUNCTIONS_PATH) { delete stripped.functions; } if (Array.isArray(stripped.authInfo) && stripped.authInfo.length === 0) { delete stripped.authInfo; } if (stripped.node.externalPackages.length === 0) { delete stripped.node.externalPackages; } if (stripped.generateCommonJSApi === false) { delete stripped.generateCommonJSApi; } // Remove "node" field if it has nothing nested under it if (Object.keys(stripped.node).length === 0) { delete stripped.node; } if (stripped.codegen.staticApi === false) { delete stripped.codegen.staticApi; } if (stripped.codegen.staticDataModel === false) { delete stripped.codegen.staticDataModel; } if (Object.keys(stripped.codegen).length === 0) { delete stripped.codegen; } return stripped; } function filterWriteableConfig(projectConfig: any) { const writeable: any = { ...projectConfig }; delete writeable.project; delete writeable.team; delete writeable.prodUrl; return writeable; } export function removedExistingConfig( ctx: Context, configPath: string, options: { allowExistingConfig?: boolean }, ) { if (!options.allowExistingConfig) { return false; } recursivelyDelete(ctx, configPath); logFinishedStep(`Removed existing ${configPath}`); return true; } /** Pull configuration from the given remote origin. */ export async function pullConfig( ctx: Context, project: string | undefined, team: string | undefined, origin: string, adminKey: string, ): Promise<ConfigWithModuleHashes> { const fetch = deploymentFetch(ctx, { deploymentUrl: origin, adminKey, }); changeSpinner("Downloading current deployment state..."); try { const res = await fetch("/api/get_config_hashes", { method: "POST", body: JSON.stringify({ version, adminKey }), }); deprecationCheckWarning(ctx, res); const data = await res.json(); const backendConfig = parseBackendConfig(data.config); const projectConfig = { ...backendConfig, node: { // This field is not stored in the backend, which is ok since it is also // not used to diff configs. externalPackages: [], nodeVersion: data.nodeVersion, }, // This field is not stored in the backend, it only affects the client. generateCommonJSApi: false, // This field is also not stored in the backend, it only affects the client. codegen: { staticApi: false, staticDataModel: false, }, project, team, prodUrl: origin, }; return { projectConfig, moduleHashes: data.moduleHashes, // TODO(presley): Add this to diffConfig(). nodeDependencies: data.nodeDependencies, udfServerVersion: data.udfServerVersion, }; } catch (err: unknown) { logFailure(`Error: Unable to pull deployment config from ${origin}`); return await logAndHandleFetchError(ctx, err); } } interface BundledModuleInfo { name: string; platform: "node" | "convex"; } /** * A component definition spec contains enough information to create bundles * of code that must be analyzed in order to construct a ComponentDefinition. * * Most paths are relative to the directory of the definitionPath. */ export type ComponentDefinitionSpec = { /** This path is relative to the app (root component) directory. */ definitionPath: ComponentDefinitionPath; /** Dependencies are paths to the directory of the dependency component definition from the app (root component) directory */ dependencies: ComponentDefinitionPath[]; // All other paths are relative to the directory of the definitionPath above. definition: Bundle; schema: Bundle; functions: Bundle[]; }; export type AppDefinitionSpec = Omit< ComponentDefinitionSpec, "definitionPath" > & { // Only app (root) component specs contain an auth bundle. auth: Bundle | null; }; export type ComponentDefinitionSpecWithoutImpls = Omit< ComponentDefinitionSpec, "schema" | "functions" >; export type AppDefinitionSpecWithoutImpls = Omit< AppDefinitionSpec, "schema" | "functions" | "auth" >; export function configJSON( config: Config, adminKey: string, schemaId?: string, pushMetrics?: PushMetrics, bundledModuleInfos?: BundledModuleInfo[], ) { // Override origin with the url const projectConfig = { projectSlug: config.projectConfig.project, teamSlug: config.projectConfig.team, functions: config.projectConfig.functions, authInfo: config.projectConfig.authInfo, }; return { config: projectConfig, modules: config.modules, nodeDependencies: config.nodeDependencies, udfServerVersion: config.udfServerVersion, schemaId, adminKey, pushMetrics, bundledModuleInfos, nodeVersion: config.nodeVersion, }; } // Time in seconds of various spans of time during a push. export type PushMetrics = { typecheck: number; bundle: number; schemaPush: number; codePull: number; totalBeforePush: number; }; /** Push configuration to the given remote origin. */ export async function pushConfig( ctx: Context, config: Config, options: { adminKey: string; url: string; deploymentName: string | null; pushMetrics?: PushMetrics | undefined; schemaId?: string | undefined; bundledModuleInfos?: BundledModuleInfo[]; }, ): Promise<void> { const serializedConfig = configJSON( config, options.adminKey, options.schemaId, options.pushMetrics, options.bundledModuleInfos, ); const fetch = deploymentFetch(ctx, { deploymentUrl: options.url, adminKey: options.adminKey, }); try { if (config.nodeDependencies.length > 0) { changeSpinner( "Installing external packages and deploying source code...", ); } else { changeSpinner("Analyzing and deploying source code..."); } await fetch("/api/push_config", { body: await brotli(JSON.stringify(serializedConfig), { params: { [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, [zlib.constants.BROTLI_PARAM_QUALITY]: 4, }, }), method: "POST", headers: { "Content-Type": "application/json", "Content-Encoding": "br", }, }); } catch (error: unknown) { await handlePushConfigError( ctx, error, "Error: Unable to push deployment config to " + options.url, options.deploymentName, { adminKey: options.adminKey, deploymentUrl: options.url, deploymentNotice: "", }, ); } } type Files = { source: string; filename: string }[]; export type CodegenResponse = | { success: true; files: Files; } | { success: false; error: string; }; function renderModule(module: { path: string; sourceMapSize: number; sourceSize: number; }): string { return ( module.path + ` (${formatSize(module.sourceSize)}, source map ${module.sourceMapSize})` ); } function hash(bundle: Bundle) { return createHash("sha256") .update(bundle.source) .update(bundle.sourceMap || "") .digest("hex"); } type ModuleDiffStat = { count: number; size: number }; export type ModuleDiffStats = { updated: ModuleDiffStat; identical: ModuleDiffStat; added: ModuleDiffStat; numDropped: number; }; function compareModules( oldModules: BundleHash[], newModules: Bundle[], ): { diffString: string; stats: ModuleDiffStats; } { let diff = ""; const oldModuleMap = new Map( oldModules.map((value) => [value.path, value.hash]), ); const newModuleMap = new Map( newModules.map((value) => [ value.path, { hash: hash(value), sourceMapSize: value.sourceMap?.length ?? 0, sourceSize: value.source.length, }, ]), ); const updatedModules: Array<{ path: string; sourceMapSize: number; sourceSize: number; }> = []; const identicalModules: Array<{ path: string; size: number }> = []; const droppedModules: Array<string> = []; const addedModules: Array<{ path: string; sourceMapSize: number; sourceSize: number; }> = []; for (const [path, oldHash] of oldModuleMap.entries()) { const newModule = newModuleMap.get(path); if (newModule === undefined) { droppedModules.push(path); } else if (newModule.hash !== oldHash) { updatedModules.push({ path, sourceMapSize: newModule.sourceMapSize, sourceSize: newModule.sourceSize, }); } else { identicalModules.push({ path, size: newModule.sourceSize + newModule.sourceMapSize, }); } } for (const [path, newModule] of newModuleMap.entries()) { if (oldModuleMap.get(path) === undefined) { addedModules.push({ path, sourceMapSize: newModule.sourceMapSize, sourceSize: newModule.sourceSize, }); } } if (droppedModules.length > 0 || updatedModules.length > 0) { diff += "Delete the following modules:\n"; for (const module of droppedModules) { diff += `[-] ${module}\n`; } for (const module of updatedModules) { diff += `[-] ${module.path}\n`; } } if (addedModules.length > 0 || updatedModules.length > 0) { diff += "Add the following modules:\n"; for (const module of addedModules) { diff += "[+] " + renderModule(module) + "\n"; } for (const module of updatedModules) { diff += "[+] " + renderModule(module) + "\n"; } } return { diffString: diff, stats: { updated: { count: updatedModules.length, size: updatedModules.reduce((acc, curr) => { return acc + curr.sourceMapSize + curr.sourceSize; }, 0), }, identical: { count: identicalModules.length, size: identicalModules.reduce((acc, curr) => { return acc + curr.size; }, 0), }, added: { count: addedModules.length, size: addedModules.reduce((acc, curr) => { return acc + curr.sourceMapSize + curr.sourceSize; }, 0), }, numDropped: droppedModules.length, }, }; } /** Generate a human-readable diff between the two configs. */ export function diffConfig( oldConfig: ConfigWithModuleHashes, newConfig: Config, // We don't want to diff modules on the components push path // because it has its own diffing logic. shouldDiffModules: boolean, ): { diffString: string; stats?: ModuleDiffStats | undefined } { let diff = ""; let stats: ModuleDiffStats | undefined; if (shouldDiffModules) { const { diffString, stats: moduleStats } = compareModules( oldConfig.moduleHashes, newConfig.modules, ); diff = diffString; stats = moduleStats; } const droppedAuth = []; if ( oldConfig.projectConfig.authInfo !== undefined && newConfig.projectConfig.authInfo !== undefined ) { for (const oldAuth of oldConfig.projectConfig.authInfo) { let matches = false; for (const newAuth of newConfig.projectConfig.authInfo) { if (equal(oldAuth, newAuth)) { matches = true; break; } } if (!matches) { droppedAuth.push(oldAuth); } } if (droppedAuth.length > 0) { diff += "Remove the following auth providers:\n"; for (const authInfo of droppedAuth) { diff += "[-] " + JSON.stringify(authInfo) + "\n"; } } const addedAuth = []; for (const newAuth of newConfig.projectConfig.authInfo) { let matches = false; for (const oldAuth of oldConfig.projectConfig.authInfo) { if (equal(newAuth, oldAuth)) { matches = true; break; } } if (!matches) { addedAuth.push(newAuth); } } if (addedAuth.length > 0) { diff += "Add the following auth providers:\n"; for (const auth of addedAuth) { diff += "[+] " + JSON.stringify(auth) + "\n"; } } } else if ( (oldConfig.projectConfig.authInfo !== undefined) !== (newConfig.projectConfig.authInfo !== undefined) ) { diff += "Moved auth config into auth.config.ts\n"; } let versionMessage = ""; const matches = oldConfig.udfServerVersion === newConfig.udfServerVersion; if (oldConfig.udfServerVersion && (!newConfig.udfServerVersion || !matches)) { versionMessage += `[-] ${oldConfig.udfServerVersion}\n`; } if (newConfig.udfServerVersion && (!oldConfig.udfServerVersion || !matches)) { versionMessage += `[+] ${newConfig.udfServerVersion}\n`; } if (versionMessage) { diff += "Change the server's function version:\n"; diff += versionMessage; } if (oldConfig.projectConfig.node.nodeVersion !== newConfig.nodeVersion) { diff += "Change the server's version for Node.js actions:\n"; if (oldConfig.projectConfig.node.nodeVersion) { diff += `[-] ${oldConfig.projectConfig.node.nodeVersion}\n`; } if (newConfig.nodeVersion) { diff += `[+] ${newConfig.nodeVersion}\n`; } } return { diffString: diff, stats }; } export async function handlePushConfigError( ctx: Context, error: unknown, defaultMessage: string, deploymentName: string | null, deployment?: { deploymentUrl: string; adminKey: string; deploymentNotice: string; }, ): Promise<never> { const data: ErrorData | undefined = error instanceof ThrowingFetchError ? error.serverErrorData : undefined; if (data?.code === "AuthConfigMissingEnvironmentVariable") { const errorMessage = data.message || "(no error message given)"; const [, variableName] = errorMessage.match(/Environment variable (\S+)/i) ?? []; // WORKOS_CLIENT_ID is a special environment variable because cloud Convex // deployments may be able to supply it by provisioning a fresh WorkOS // environment on demand. if (variableName === "WORKOS_CLIENT_ID" && deploymentName && deployment) { // Initially only specific templates create WorkOS environments on demand // because the local environemnt variables are hardcoded for Vite and Next.js. const homepage = await currentPackageHomepage(ctx); const autoProvisionIfWorkOSTeamAssociated = !!( homepage && [ "https://github.com/workos/template-convex-nextjs-authkit/#readme", "https://github.com/workos/template-convex-react-vite-authkit/#readme", "https://github.com:workos/template-convex-react-vite-authkit/#readme", ].includes(homepage) ); // Initially only specific templates offer team creation. // Until this changes it can be done manually with a CLI command. const offerToAssociateWorkOSTeam = autoProvisionIfWorkOSTeamAssociated; // Initialy only specific template auto-configure WorkOS environments // with AuthKit config because these values are currently heuristics. // This will be some more explicit opt-in in the future. const autoConfigureAuthkitConfig = autoProvisionIfWorkOSTeamAssociated; const result = await ensureWorkosEnvironmentProvisioned( ctx, deploymentName, deployment, { offerToAssociateWorkOSTeam, autoProvisionIfWorkOSTeamAssociated, autoConfigureAuthkitConfig, }, ); if (result === "ready") { return await ctx.crash({ exitCode: 1, errorType: "already handled", printedMessage: null, }); } } const envVarMessage = `Environment variable ${chalk.bold( variableName, )} is used in auth config file but ` + `its value was not set.`; let setEnvVarInstructions = "Go set it in the dashboard or using `npx convex env set`"; // If `npx convex dev` is running using --url there might not be a configured deployment if (deploymentName !== null) { const variableQuery = variableName !== undefined ? `?var=${variableName}` : ""; const dashboardUrl = deploymentDashboardUrlPage( deploymentName, `/settings/environment-variables${variableQuery}`, ); setEnvVarInstructions = `Go to:\n\n ${chalk.bold( dashboardUrl, )}\n\n to set it up. `; } await ctx.crash({ exitCode: 1, errorType: "invalid filesystem or env vars", errForSentry: error, printedMessage: envVarMessage + "\n" + setEnvVarInstructions, }); } if (data?.code === "InternalServerError") { if (deploymentName?.startsWith("local-")) { printLocalDeploymentOnError(); return ctx.crash({ exitCode: 1, errorType: "fatal", errForSentry: new LocalDeploymentError( "InternalServerError while pushing to local deployment", ), printedMessage: defaultMessage, }); } } logFailure(defaultMessage); return await logAndHandleFetchError(ctx, error); }

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