Skip to main content
Glama

Convex MCP server

Official
by get-convex
api.ts23.7 kB
import { Context } from "../../bundler/context.js"; import { logVerbose, logWarning } from "../../bundler/log.js"; import { getTeamAndProjectFromPreviewAdminKey } from "./deployment.js"; import { assertLocalBackendRunning, localDeploymentUrl, } from "./localDeployment/run.js"; import { ThrowingFetchError, bigBrainAPI, bigBrainAPIMaybeThrows, logAndHandleFetchError, } from "./utils/utils.js"; import { z } from "zod"; import { DeploymentSelection, ProjectSelection, } from "./deploymentSelection.js"; import { loadLocalDeploymentCredentials } from "./localDeployment/localDeployment.js"; import { loadAnonymousDeployment } from "./localDeployment/anonymous.js"; export type DeploymentName = string; export type CloudDeploymentType = "prod" | "dev" | "preview"; export type AccountRequiredDeploymentType = CloudDeploymentType | "local"; export type DeploymentType = AccountRequiredDeploymentType | "anonymous"; export type Project = { id: number; name: string; slug: string; isDemo: boolean; }; type AdminKey = string; // Provision a new project, creating a deployment of type `deploymentTypeToProvision` export async function createProject( ctx: Context, { teamSlug: selectedTeamSlug, projectName, deploymentTypeToProvision, }: { teamSlug: string; projectName: string; deploymentTypeToProvision: "prod" | "dev"; }, ): Promise<{ projectSlug: string; teamSlug: string; projectsRemaining: number; }> { const provisioningArgs = { team: selectedTeamSlug, projectName, // TODO: Consider allowing projects with no deployments, or consider switching // to provisioning prod on creation. deploymentType: deploymentTypeToProvision, }; const data = await bigBrainAPI({ ctx, method: "POST", url: "create_project", data: provisioningArgs, }); const { projectSlug, teamSlug, projectsRemaining } = data; if ( projectSlug === undefined || teamSlug === undefined || projectsRemaining === undefined ) { const error = "Unexpected response during provisioning: " + JSON.stringify(data); return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: error, printedMessage: error, }); } return { projectSlug, teamSlug, projectsRemaining, }; } // ---------------------------------------------------------------------- // Helpers for `deploymentSelectionFromOptions` // ---------------------------------------------------------------------- export const deploymentSelectionWithinProjectSchema = z.discriminatedUnion( "kind", [ z.object({ kind: z.literal("previewName"), previewName: z.string() }), z.object({ kind: z.literal("deploymentName"), deploymentName: z.string() }), z.object({ kind: z.literal("prod") }), z.object({ kind: z.literal("implicitProd") }), z.object({ kind: z.literal("ownDev") }), ], ); export type DeploymentSelectionWithinProject = z.infer< typeof deploymentSelectionWithinProjectSchema >; type DeploymentSelectionOptionsWithinProject = { prod?: boolean | undefined; // Whether this command defaults to prod when no other flags are provided. If // this is not set, the default will be "ownDev" implicitProd?: boolean; previewName?: string | undefined; deploymentName?: string | undefined; }; export type DeploymentSelectionOptions = DeploymentSelectionOptionsWithinProject & { url?: string | undefined; adminKey?: string | undefined; envFile?: string | undefined; }; export function deploymentSelectionWithinProjectFromOptions( options: DeploymentSelectionOptions, ): DeploymentSelectionWithinProject { if (options.previewName !== undefined) { return { kind: "previewName", previewName: options.previewName }; } if (options.deploymentName !== undefined) { return { kind: "deploymentName", deploymentName: options.deploymentName }; } if (options.prod) { return { kind: "prod" }; } if (options.implicitProd) { return { kind: "implicitProd" }; } return { kind: "ownDev" }; } export async function validateDeploymentSelectionForExistingDeployment( ctx: Context, deploymentSelection: DeploymentSelectionWithinProject, source: "selfHosted" | "deployKey" | "cliArgs", ) { if ( deploymentSelection.kind === "ownDev" || deploymentSelection.kind === "implicitProd" ) { // These are both considered the "default" selection depending on the command, so this is always fine return; } switch (source) { case "selfHosted": return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "The `--prod`, `--preview-name`, and `--deployment-name` flags cannot be used with a self-hosted deployment.", }); case "deployKey": logWarning( "Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags and using deployment from CONVEX_DEPLOY_KEY", ); break; case "cliArgs": logWarning( "Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags since this command was run with --url and --admin-key", ); break; } } // ---------------------------------------------------------------------- // Helpers for `checkAccessToSelectedProject` // ---------------------------------------------------------------------- async function hasAccessToProject( ctx: Context, selector: { projectSlug: string; teamSlug: string }, ): Promise<boolean> { try { await bigBrainAPIMaybeThrows({ ctx, url: `teams/${selector.teamSlug}/projects/${selector.projectSlug}/deployments`, method: "GET", }); return true; } catch (err) { if ( err instanceof ThrowingFetchError && (err.serverErrorData?.code === "TeamNotFound" || err.serverErrorData?.code === "ProjectNotFound") ) { return false; } return logAndHandleFetchError(ctx, err); } } export async function checkAccessToSelectedProject( ctx: Context, projectSelection: ProjectSelection, ): Promise< | { kind: "hasAccess"; teamSlug: string; projectSlug: string } | { kind: "noAccess" } | { kind: "unknown" } > { switch (projectSelection.kind) { case "deploymentName": { const result = await getTeamAndProjectSlugForDeployment(ctx, { deploymentName: projectSelection.deploymentName, }); if (result === null) { return { kind: "noAccess" }; } return { kind: "hasAccess", teamSlug: result.teamSlug, projectSlug: result.projectSlug, }; } case "teamAndProjectSlugs": { const hasAccess = await hasAccessToProject(ctx, { teamSlug: projectSelection.teamSlug, projectSlug: projectSelection.projectSlug, }); if (!hasAccess) { return { kind: "noAccess" }; } return { kind: "hasAccess", teamSlug: projectSelection.teamSlug, projectSlug: projectSelection.projectSlug, }; } case "projectDeployKey": // Ideally we would be able to do an explicit check here, but if the key is invalid, // it will instead fail as soon as we try to use the key. return { kind: "unknown" }; default: { projectSelection satisfies never; return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: `Invalid project selection: ${(projectSelection as any).kind}`, }); } } } export async function getTeamAndProjectSlugForDeployment( ctx: Context, selector: { deploymentName: string }, ): Promise<{ teamSlug: string; projectSlug: string } | null> { try { const body = await bigBrainAPIMaybeThrows({ ctx, url: `deployment/${selector.deploymentName}/team_and_project`, method: "GET", }); return { teamSlug: body.team, projectSlug: body.project }; } catch (err) { if ( err instanceof ThrowingFetchError && (err.serverErrorData?.code === "DeploymentNotFound" || err.serverErrorData?.code === "ProjectNotFound") ) { return null; } return logAndHandleFetchError(ctx, err); } } // ---------------------------------------------------------------------- // Helpers for fetching deployment credentials // ---------------------------------------------------------------------- // Used by dev for upgrade from team and project in convex.json to CONVEX_DEPLOYMENT export async function fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows( ctx: Context, projectSelection: | { kind: "teamAndProjectSlugs"; teamSlug: string; projectSlug: string } | { kind: "projectDeployKey"; projectDeployKey: string }, deploymentType: "prod" | "dev", ): Promise<{ deploymentName: string; deploymentUrl: string; adminKey: AdminKey; }> { if (projectSelection.kind === "projectDeployKey") { const auth = ctx.bigBrainAuth(); const doesAuthMatch = auth !== null && auth.kind === "projectKey" && auth.projectKey === projectSelection.projectDeployKey; if (!doesAuthMatch) { return await ctx.crash({ exitCode: 1, errorType: "fatal", errForSentry: new Error( "Expected project deploy key to match the big brain auth header", ), printedMessage: "Unexpected error when loading the Convex deployment", }); } } let data; try { data = await bigBrainAPIMaybeThrows({ ctx, method: "POST", url: "deployment/provision_and_authorize", data: { teamSlug: projectSelection.kind === "teamAndProjectSlugs" ? projectSelection.teamSlug : null, projectSlug: projectSelection.kind === "teamAndProjectSlugs" ? projectSelection.projectSlug : null, deploymentType: deploymentType === "prod" ? "prod" : "dev", }, }); } catch (error) { const msg = "Unknown error during authorization: " + error; return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: new Error(msg), printedMessage: msg, }); } const adminKey = data.adminKey; const url = data.url; const deploymentName = data.deploymentName; if (adminKey === undefined || url === undefined) { const msg = "Unknown error during authorization: " + JSON.stringify(data); return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: new Error(msg), printedMessage: msg, }); } return { adminKey, deploymentUrl: url, deploymentName }; } async function fetchExistingDevDeploymentCredentialsOrCrash( ctx: Context, deploymentName: DeploymentName, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: DeploymentType; }> { const slugs = await fetchTeamAndProject(ctx, deploymentName); const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows( ctx, { kind: "teamAndProjectSlugs", teamSlug: slugs.team, projectSlug: slugs.project, }, "dev", ); return { deploymentName: credentials.deploymentName, adminKey: credentials.adminKey, url: credentials.deploymentUrl, deploymentType: "dev", }; } // ---------------------------------------------------------------------- // Helpers for `loadSelectedDeploymentCredentials` // ---------------------------------------------------------------------- async function handleOwnDev( ctx: Context, projectSelection: ProjectSelection, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: DeploymentType; }> { switch (projectSelection.kind) { case "deploymentName": { if (projectSelection.deploymentType === "local") { const credentials = await loadLocalDeploymentCredentials( ctx, projectSelection.deploymentName, ); return { deploymentName: projectSelection.deploymentName, adminKey: credentials.adminKey, url: credentials.deploymentUrl, deploymentType: "local", }; } return await fetchExistingDevDeploymentCredentialsOrCrash( ctx, projectSelection.deploymentName, ); } case "teamAndProjectSlugs": case "projectDeployKey": { // Note -- this provisions a dev deployment if one doesn't exist const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows( ctx, projectSelection, "dev", ); return { url: credentials.deploymentUrl, adminKey: credentials.adminKey, deploymentName: credentials.deploymentName, deploymentType: "dev", }; } default: { projectSelection satisfies never; return ctx.crash({ exitCode: 1, errorType: "fatal", // This should be unreachable, so don't bother with a printed message. printedMessage: null, errForSentry: `Unexpected project selection: ${(projectSelection as any).kind}`, }); } } } async function handleProd( ctx: Context, projectSelection: ProjectSelection, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: "prod"; }> { switch (projectSelection.kind) { case "deploymentName": { const credentials = await bigBrainAPI({ ctx, method: "POST", url: "deployment/authorize_prod", data: { deploymentName: projectSelection.deploymentName, }, }); return credentials; } case "teamAndProjectSlugs": case "projectDeployKey": { const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows( ctx, projectSelection, "prod", ); return { url: credentials.deploymentUrl, adminKey: credentials.adminKey, deploymentName: credentials.deploymentName, deploymentType: "prod", }; } } } async function handlePreview( ctx: Context, previewName: string, projectSelection: ProjectSelection, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: "preview"; }> { switch (projectSelection.kind) { case "deploymentName": case "teamAndProjectSlugs": return await bigBrainAPI({ ctx, method: "POST", url: "deployment/authorize_preview", data: { previewName: previewName, projectSelection: projectSelection, }, }); case "projectDeployKey": // TODO -- this should be supported return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Project deploy keys are not supported for preview deployments", }); } } async function handleDeploymentName( ctx: Context, deploymentName: string, projectSelection: ProjectSelection, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: DeploymentType; }> { switch (projectSelection.kind) { case "deploymentName": case "teamAndProjectSlugs": return await bigBrainAPI({ ctx, method: "POST", url: "deployment/authorize_within_current_project", data: { selectedDeploymentName: deploymentName, projectSelection: projectSelection, }, }); case "projectDeployKey": // TODO -- this should be supported return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Project deploy keys are not supported with the --deployment-name flag", }); } } async function fetchDeploymentCredentialsWithinCurrentProject( ctx: Context, projectSelection: ProjectSelection, deploymentSelection: DeploymentSelectionWithinProject, ): Promise<{ deploymentName: string; adminKey: string; url: string; deploymentType: DeploymentType; }> { switch (deploymentSelection.kind) { case "ownDev": { return await handleOwnDev(ctx, projectSelection); } case "implicitProd": case "prod": { return await handleProd(ctx, projectSelection); } case "previewName": return await handlePreview( ctx, deploymentSelection.previewName, projectSelection, ); case "deploymentName": return await handleDeploymentName( ctx, deploymentSelection.deploymentName, projectSelection, ); default: { deploymentSelection satisfies never; return ctx.crash({ exitCode: 1, errorType: "fatal", // This should be unreachable, so don't bother with a printed message. printedMessage: null, errForSentry: `Unexpected deployment selection: ${deploymentSelection as any}`, }); } } } async function _loadExistingDeploymentCredentialsForProject( ctx: Context, targetProject: ProjectSelection, deploymentSelection: DeploymentSelectionWithinProject, { ensureLocalRunning } = { ensureLocalRunning: true }, ): Promise<{ adminKey: string; url: string; deploymentFields: { deploymentName: string; deploymentType: DeploymentType; projectSlug: string | null; teamSlug: string | null; } | null; }> { const accessResult = await checkAccessToSelectedProject(ctx, targetProject); if (accessResult.kind === "noAccess") { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "You don't have access to the selected project. Run `npx convex dev` to select a different project.", }); } const result = await fetchDeploymentCredentialsWithinCurrentProject( ctx, targetProject, deploymentSelection, ); logVerbose( `Deployment URL: ${result.url}, Deployment Name: ${result.deploymentName}, Deployment Type: ${result.deploymentType}`, ); if (ensureLocalRunning && result.deploymentType === "local") { await assertLocalBackendRunning(ctx, { url: result.url, deploymentName: result.deploymentName, }); } return { ...result, deploymentFields: { deploymentName: result.deploymentName, deploymentType: result.deploymentType, projectSlug: accessResult.kind === "hasAccess" ? accessResult.projectSlug : null, teamSlug: accessResult.kind === "hasAccess" ? accessResult.teamSlug : null, }, }; } export type DetailedDeploymentCredentials = { adminKey: string; url: string; deploymentFields: { deploymentName: string; deploymentType: DeploymentType; projectSlug: string | null; teamSlug: string | null; } | null; }; // This is used by most commands (notably not `dev` and `deploy`) to determine // which deployment to act on, taking into account the deployment selection flags. // export async function loadSelectedDeploymentCredentials( ctx: Context, deploymentSelection: DeploymentSelection, selectionWithinProject: DeploymentSelectionWithinProject, { ensureLocalRunning } = { ensureLocalRunning: true }, ): Promise<DetailedDeploymentCredentials> { switch (deploymentSelection.kind) { case "existingDeployment": await validateDeploymentSelectionForExistingDeployment( ctx, selectionWithinProject, deploymentSelection.deploymentToActOn.source, ); // We're already set up. logVerbose( `Deployment URL: ${deploymentSelection.deploymentToActOn.url}, Deployment Name: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentName ?? "unknown"}, Deployment Type: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentType ?? "unknown"}`, ); return { adminKey: deploymentSelection.deploymentToActOn.adminKey, url: deploymentSelection.deploymentToActOn.url, deploymentFields: deploymentSelection.deploymentToActOn.deploymentFields, }; case "chooseProject": return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project", }); case "preview": { const slugs = await getTeamAndProjectFromPreviewAdminKey( ctx, deploymentSelection.previewDeployKey, ); return await _loadExistingDeploymentCredentialsForProject( ctx, { kind: "teamAndProjectSlugs", teamSlug: slugs.teamSlug, projectSlug: slugs.projectSlug, }, selectionWithinProject, { ensureLocalRunning }, ); } case "deploymentWithinProject": { return await _loadExistingDeploymentCredentialsForProject( ctx, deploymentSelection.targetProject, selectionWithinProject, { ensureLocalRunning }, ); } case "anonymous": { if (deploymentSelection.deploymentName === null) { return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project", }); } const config = await loadAnonymousDeployment( ctx, deploymentSelection.deploymentName, ); const url = localDeploymentUrl(config.ports.cloud); if (ensureLocalRunning) { await assertLocalBackendRunning(ctx, { url, deploymentName: deploymentSelection.deploymentName, }); } return { adminKey: config.adminKey, url, deploymentFields: { deploymentName: deploymentSelection.deploymentName, deploymentType: "anonymous", projectSlug: null, teamSlug: null, }, }; } default: { deploymentSelection satisfies never; return await ctx.crash({ exitCode: 1, errorType: "fatal", printedMessage: "Unknown deployment type", }); } } } export async function fetchTeamAndProject( ctx: Context, deploymentName: string, ) { const data = (await bigBrainAPI({ ctx, method: "GET", url: `deployment/${deploymentName}/team_and_project`, })) as { team: string; // slug project: string; // slug teamId: number; projectId: number; }; const { team, project } = data; if (team === undefined || project === undefined) { const msg = "Unknown error when fetching team and project: " + JSON.stringify(data); return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: new Error(msg), printedMessage: msg, }); } return data; } export async function fetchTeamAndProjectForKey( ctx: Context, // Deployment deploy key, like `prod:happy-animal-123|<stuff>` deployKey: string, ) { const data = (await bigBrainAPI({ ctx, method: "POST", url: `deployment/team_and_project_for_key`, data: { deployKey: deployKey, }, })) as { team: string; // slug project: string; // slug teamId: number; projectId: number; }; const { team, project } = data; if (team === undefined || project === undefined) { const msg = "Unknown error when fetching team and project: " + JSON.stringify(data); return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: new Error(msg), printedMessage: msg, }); } return data; } export async function getTeamsForUser(ctx: Context) { const teams = await bigBrainAPI<{ id: number; name: string; slug: string }[]>( { ctx, method: "GET", url: "teams", }, ); return teams; }

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